mirror of
https://github.com/meilisearch/MeiliSearch
synced 2025-01-09 04:54:31 +01:00
Merge #4867
4867: Autogenerate the openAPI spec r=irevoire a=irevoire # Pull Request ## Related issue Fixes https://github.com/meilisearch/meilisearch/issues/5073 ## What does this PR do? - Introduce utoipa and the auto-generation of the openAPI file - Introduce the scalar swagger when the `swagger` feature flag is enabled. Generating the openAPI file takes between 15 and 20ms at startup time on my computer. That could be an issue if we plan to stabilize the feature. Co-authored-by: Tamo <tamo@meilisearch.com>
This commit is contained in:
commit
0e6b6bd130
41
Cargo.lock
generated
41
Cargo.lock
generated
@ -3532,6 +3532,8 @@ dependencies = [
|
||||
"tracing-trace",
|
||||
"url",
|
||||
"urlencoding",
|
||||
"utoipa",
|
||||
"utoipa-scalar",
|
||||
"uuid",
|
||||
"wiremock",
|
||||
"yaup",
|
||||
@ -3587,6 +3589,7 @@ dependencies = [
|
||||
"thiserror",
|
||||
"time",
|
||||
"tokio",
|
||||
"utoipa",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@ -3698,6 +3701,7 @@ dependencies = [
|
||||
"uell",
|
||||
"ureq",
|
||||
"url",
|
||||
"utoipa",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
@ -5953,6 +5957,43 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
|
||||
|
||||
[[package]]
|
||||
name = "utoipa"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "514a48569e4e21c86d0b84b5612b5e73c0b2cf09db63260134ba426d4e8ea714"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa-gen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-gen"
|
||||
version = "5.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5629efe65599d0ccd5d493688cbf6e03aa7c1da07fe59ff97cf5977ed0637f66"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"regex",
|
||||
"syn 2.0.87",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "utoipa-scalar"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1291aa7a2223c2f8399d1c6627ca0ba57ca0d7ecac762a2094a9dfd6376445a"
|
||||
dependencies = [
|
||||
"actix-web",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"utoipa",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.10.0"
|
||||
|
@ -5,6 +5,7 @@ use meili_snap::{json_string, snapshot};
|
||||
use meilisearch_types::milli::index::IndexEmbeddingConfig;
|
||||
use meilisearch_types::milli::update::IndexDocumentsMethod::*;
|
||||
use meilisearch_types::milli::{self};
|
||||
use meilisearch_types::settings::SettingEmbeddingSettings;
|
||||
use meilisearch_types::tasks::{IndexSwap, KindWithContent};
|
||||
use roaring::RoaringBitmap;
|
||||
|
||||
@ -647,7 +648,8 @@ fn test_settings_update() {
|
||||
response: Setting::Set(serde_json::json!("{{embedding}}")),
|
||||
..Default::default()
|
||||
};
|
||||
embedders.insert(S("default"), Setting::Set(embedding_settings));
|
||||
embedders
|
||||
.insert(S("default"), SettingEmbeddingSettings { inner: Setting::Set(embedding_settings) });
|
||||
new_settings.embedders = Setting::Set(embedders);
|
||||
|
||||
index_scheduler
|
||||
|
@ -7,7 +7,7 @@ use meilisearch_types::milli::index::IndexEmbeddingConfig;
|
||||
use meilisearch_types::milli::update::Setting;
|
||||
use meilisearch_types::milli::vector::settings::EmbeddingSettings;
|
||||
use meilisearch_types::milli::{self, obkv_to_json};
|
||||
use meilisearch_types::settings::{Settings, Unchecked};
|
||||
use meilisearch_types::settings::{SettingEmbeddingSettings, Settings, Unchecked};
|
||||
use meilisearch_types::tasks::KindWithContent;
|
||||
use milli::update::IndexDocumentsMethod::*;
|
||||
|
||||
@ -30,7 +30,10 @@ fn import_vectors() {
|
||||
response: Setting::Set(serde_json::json!("{{embedding}}")),
|
||||
..Default::default()
|
||||
};
|
||||
embedders.insert(S("A_fakerest"), Setting::Set(embedding_settings));
|
||||
embedders.insert(
|
||||
S("A_fakerest"),
|
||||
SettingEmbeddingSettings { inner: Setting::Set(embedding_settings) },
|
||||
);
|
||||
|
||||
let embedding_settings = milli::vector::settings::EmbeddingSettings {
|
||||
source: Setting::Set(milli::vector::settings::EmbedderSource::HuggingFace),
|
||||
@ -39,7 +42,10 @@ fn import_vectors() {
|
||||
document_template: Setting::Set(S("{{doc.doggo}} the {{doc.breed}} best doggo")),
|
||||
..Default::default()
|
||||
};
|
||||
embedders.insert(S("B_small_hf"), Setting::Set(embedding_settings));
|
||||
embedders.insert(
|
||||
S("B_small_hf"),
|
||||
SettingEmbeddingSettings { inner: Setting::Set(embedding_settings) },
|
||||
);
|
||||
|
||||
new_settings.embedders = Setting::Set(embedders);
|
||||
|
||||
@ -356,13 +362,13 @@ fn import_vectors_first_and_embedder_later() {
|
||||
|
||||
let setting = meilisearch_types::settings::Settings::<Unchecked> {
|
||||
embedders: Setting::Set(maplit::btreemap! {
|
||||
S("my_doggo_embedder") => Setting::Set(EmbeddingSettings {
|
||||
S("my_doggo_embedder") => SettingEmbeddingSettings { inner: Setting::Set(EmbeddingSettings {
|
||||
source: Setting::Set(milli::vector::settings::EmbedderSource::HuggingFace),
|
||||
model: Setting::Set(S("sentence-transformers/all-MiniLM-L6-v2")),
|
||||
revision: Setting::Set(S("e4ce9877abf3edfe10b0d82785e83bdcb973e22e")),
|
||||
document_template: Setting::Set(S("{{doc.doggo}}")),
|
||||
..Default::default()
|
||||
})
|
||||
}) }
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
@ -511,11 +517,11 @@ fn delete_document_containing_vector() {
|
||||
|
||||
let setting = meilisearch_types::settings::Settings::<Unchecked> {
|
||||
embedders: Setting::Set(maplit::btreemap! {
|
||||
S("manual") => Setting::Set(EmbeddingSettings {
|
||||
S("manual") => SettingEmbeddingSettings { inner: Setting::Set(EmbeddingSettings {
|
||||
source: Setting::Set(milli::vector::settings::EmbedderSource::UserProvided),
|
||||
dimensions: Setting::Set(3),
|
||||
..Default::default()
|
||||
})
|
||||
}) }
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
@ -677,18 +683,18 @@ fn delete_embedder_with_user_provided_vectors() {
|
||||
|
||||
let setting = meilisearch_types::settings::Settings::<Unchecked> {
|
||||
embedders: Setting::Set(maplit::btreemap! {
|
||||
S("manual") => Setting::Set(EmbeddingSettings {
|
||||
S("manual") => SettingEmbeddingSettings { inner: Setting::Set(EmbeddingSettings {
|
||||
source: Setting::Set(milli::vector::settings::EmbedderSource::UserProvided),
|
||||
dimensions: Setting::Set(3),
|
||||
..Default::default()
|
||||
}),
|
||||
S("my_doggo_embedder") => Setting::Set(EmbeddingSettings {
|
||||
}) },
|
||||
S("my_doggo_embedder") => SettingEmbeddingSettings { inner: Setting::Set(EmbeddingSettings {
|
||||
source: Setting::Set(milli::vector::settings::EmbedderSource::HuggingFace),
|
||||
model: Setting::Set(S("sentence-transformers/all-MiniLM-L6-v2")),
|
||||
revision: Setting::Set(S("e4ce9877abf3edfe10b0d82785e83bdcb973e22e")),
|
||||
document_template: Setting::Set(S("{{doc.doggo}}")),
|
||||
..Default::default()
|
||||
}),
|
||||
}) },
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
@ -764,7 +770,7 @@ fn delete_embedder_with_user_provided_vectors() {
|
||||
{
|
||||
let setting = meilisearch_types::settings::Settings::<Unchecked> {
|
||||
embedders: Setting::Set(maplit::btreemap! {
|
||||
S("manual") => Setting::Reset,
|
||||
S("manual") => SettingEmbeddingSettings { inner: Setting::Reset },
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
|
@ -40,6 +40,7 @@ time = { version = "0.3.36", features = [
|
||||
"macros",
|
||||
] }
|
||||
tokio = "1.38"
|
||||
utoipa = { version = "5.2.0", features = ["macros"] }
|
||||
uuid = { version = "1.10.0", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -1,13 +1,15 @@
|
||||
use milli::progress::ProgressView;
|
||||
use serde::Serialize;
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::batches::{Batch, BatchId, BatchStats};
|
||||
use crate::task_view::DetailsView;
|
||||
use crate::tasks::serialize_duration;
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct BatchView {
|
||||
pub uid: BatchId,
|
||||
pub progress: Option<ProgressView>,
|
||||
|
@ -3,6 +3,7 @@ use std::collections::BTreeMap;
|
||||
use milli::progress::ProgressView;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::task_view::DetailsView;
|
||||
use crate::tasks::{Kind, Status};
|
||||
@ -25,8 +26,9 @@ pub struct Batch {
|
||||
pub finished_at: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Default, Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct BatchStats {
|
||||
pub total_nb_tasks: BatchId,
|
||||
pub status: BTreeMap<Status, u32>,
|
||||
|
@ -7,17 +7,25 @@ use aweb::rt::task::JoinError;
|
||||
use convert_case::Casing;
|
||||
use milli::heed::{Error as HeedError, MdbError};
|
||||
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")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct ResponseError {
|
||||
#[serde(skip)]
|
||||
pub code: StatusCode,
|
||||
/// The error message.
|
||||
pub message: String,
|
||||
/// The error code.
|
||||
#[schema(value_type = Code)]
|
||||
#[serde(rename = "code")]
|
||||
error_code: String,
|
||||
/// The error type.
|
||||
#[schema(value_type = ErrorType)]
|
||||
#[serde(rename = "type")]
|
||||
error_type: String,
|
||||
/// A link to the documentation about this specific error.
|
||||
#[serde(rename = "link")]
|
||||
error_link: String,
|
||||
}
|
||||
@ -97,7 +105,9 @@ pub trait ErrorCode {
|
||||
}
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
enum ErrorType {
|
||||
#[derive(ToSchema)]
|
||||
#[schema(rename_all = "snake_case")]
|
||||
pub enum ErrorType {
|
||||
Internal,
|
||||
InvalidRequest,
|
||||
Auth,
|
||||
@ -129,7 +139,8 @@ impl fmt::Display for ErrorType {
|
||||
/// `MyErrorCode::default().error_code()`.
|
||||
macro_rules! make_error_codes {
|
||||
($($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 {
|
||||
$($code_ident),*
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
use deserr::Deserr;
|
||||
use milli::OrderBy;
|
||||
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")]
|
||||
#[deserr(rename_all = camelCase)]
|
||||
pub enum FacetValuesSort {
|
||||
|
@ -4,13 +4,15 @@ use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
||||
use deserr::Deserr;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::error::{Code, ErrorCode};
|
||||
|
||||
/// An index uid is composed of only ascii alphanumeric characters, - and _, between 1 and 400
|
||||
/// 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)]
|
||||
#[schema(value_type = String, example = "movies")]
|
||||
pub struct IndexUid(String);
|
||||
|
||||
impl IndexUid {
|
||||
|
@ -9,6 +9,7 @@ use serde::{Deserialize, Serialize};
|
||||
use time::format_description::well_known::Rfc3339;
|
||||
use time::macros::{format_description, time};
|
||||
use time::{Date, OffsetDateTime, PrimitiveDateTime};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
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)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct CreateApiKey {
|
||||
/// A description for the key. `null` if empty.
|
||||
#[schema(example = json!(null))]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidApiKeyDescription>)]
|
||||
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>)]
|
||||
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)]
|
||||
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)]
|
||||
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)]
|
||||
#[schema(value_type = Vec<String>, example = json!(["products"]))]
|
||||
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)]
|
||||
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)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct PatchApiKey {
|
||||
#[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>,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidApiKeyName>)]
|
||||
#[schema(value_type = Option<String>, example = "Indexing Products API key")]
|
||||
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)]
|
||||
pub enum Action {
|
||||
#[serde(rename = "*")]
|
||||
|
@ -1,8 +1,9 @@
|
||||
use deserr::Deserr;
|
||||
use milli::LocalizedAttributesRule;
|
||||
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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct LocalizedAttributesRuleView {
|
||||
@ -33,7 +34,7 @@ impl From<LocalizedAttributesRuleView> for LocalizedAttributesRule {
|
||||
/// this enum implements `Deserr` in order to be used in the API.
|
||||
macro_rules! make_locale {
|
||||
($(($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)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum Locale {
|
||||
|
@ -13,6 +13,7 @@ use milli::proximity::ProximityPrecision;
|
||||
use milli::update::Setting;
|
||||
use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET};
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::deserr::DeserrJsonError;
|
||||
use crate::error::deserr_codes::*;
|
||||
@ -39,10 +40,10 @@ where
|
||||
.serialize(s)
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Debug, Serialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Default, Debug, Serialize, PartialEq, Eq, ToSchema)]
|
||||
pub struct Checked;
|
||||
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq, Eq, ToSchema)]
|
||||
pub struct Unchecked;
|
||||
|
||||
impl<E> Deserr<E> for Unchecked
|
||||
@ -69,54 +70,63 @@ fn validate_min_word_size_for_typo_setting<E: DeserializeError>(
|
||||
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")]
|
||||
#[deserr(deny_unknown_fields, rename_all = camelCase, validate = validate_min_word_size_for_typo_setting -> DeserrJsonError<InvalidSettingsTypoTolerance>)]
|
||||
pub struct MinWordSizeTyposSetting {
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<u8>, example = json!(5))]
|
||||
pub one_typo: Setting<u8>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<u8>, example = json!(9))]
|
||||
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")]
|
||||
#[deserr(deny_unknown_fields, rename_all = camelCase, where_predicate = __Deserr_E: deserr::MergeWithError<DeserrJsonError<InvalidSettingsTypoTolerance>>)]
|
||||
pub struct TypoSettings {
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<bool>, example = json!(true))]
|
||||
pub enabled: Setting<bool>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
|
||||
#[schema(value_type = Option<MinWordSizeTyposSetting>, example = json!({ "oneTypo": 5, "twoTypo": 9 }))]
|
||||
pub min_word_size_for_typos: Setting<MinWordSizeTyposSetting>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<BTreeSet<String>>, example = json!(["iPhone", "phone"]))]
|
||||
pub disable_on_words: Setting<BTreeSet<String>>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<BTreeSet<String>>, example = json!(["uuid", "url"]))]
|
||||
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")]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct FacetingSettings {
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<usize>, example = json!(10))]
|
||||
pub max_values_per_facet: Setting<usize>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<BTreeMap<String, FacetValuesSort>>, example = json!({ "genre": FacetValuesSort::Count }))]
|
||||
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")]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct PaginationSettings {
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<usize>, example = json!(250))]
|
||||
pub max_total_hits: Setting<usize>,
|
||||
}
|
||||
|
||||
@ -134,79 +144,149 @@ impl MergeWithError<milli::CriterionError> for DeserrJsonError<InvalidSettingsRa
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, Deserialize, PartialEq, Eq, Clone, ToSchema)]
|
||||
#[repr(transparent)]
|
||||
#[serde(transparent)]
|
||||
/// "Technical" type that is required due to utoipa.
|
||||
///
|
||||
/// We did not find a way to implement [`utoipa::ToSchema`] for the [`Setting`] enum,
|
||||
/// but most types can use the `value_type` macro parameter to workaround that issue.
|
||||
///
|
||||
/// However that type is used in the settings route, including through the macro that auto-generate
|
||||
/// all the settings route, so we can't remap the `value_type`.
|
||||
pub struct SettingEmbeddingSettings {
|
||||
#[schema(inline, value_type = Option<crate::milli::vector::settings::EmbeddingSettings>)]
|
||||
pub inner: Setting<crate::milli::vector::settings::EmbeddingSettings>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for SettingEmbeddingSettings {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
self.inner.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: DeserializeError> Deserr<E> for SettingEmbeddingSettings {
|
||||
fn deserialize_from_value<V: deserr::IntoValue>(
|
||||
value: deserr::Value<V>,
|
||||
location: ValuePointerRef,
|
||||
) -> Result<Self, E> {
|
||||
Setting::<crate::milli::vector::settings::EmbeddingSettings>::deserialize_from_value(
|
||||
value, location,
|
||||
)
|
||||
.map(|inner| Self { inner })
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds all the settings for an index. `T` can either be `Checked` if they represents settings
|
||||
/// whose validity is guaranteed, or `Unchecked` if they need to be validated. In the later case, a
|
||||
/// call to `check` will return a `Settings<Checked>` from a `Settings<Unchecked>`.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)]
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
|
||||
#[serde(
|
||||
deny_unknown_fields,
|
||||
rename_all = "camelCase",
|
||||
bound(serialize = "T: Serialize", deserialize = "T: Deserialize<'static>")
|
||||
)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct Settings<T> {
|
||||
/// Fields displayed in the returned documents.
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsDisplayedAttributes>)]
|
||||
#[schema(value_type = Option<Vec<String>>, example = json!(["id", "title", "description", "url"]))]
|
||||
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")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchableAttributes>)]
|
||||
#[schema(value_type = Option<Vec<String>>, example = json!(["title", "description"]))]
|
||||
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")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsFilterableAttributes>)]
|
||||
#[schema(value_type = Option<Vec<String>>, example = json!(["release_date", "genre"]))]
|
||||
pub filterable_attributes: Setting<BTreeSet<String>>,
|
||||
/// Attributes to use when sorting search results.
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSortableAttributes>)]
|
||||
#[schema(value_type = Option<Vec<String>>, example = json!(["release_date"]))]
|
||||
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")]
|
||||
#[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>>,
|
||||
/// List of words ignored when present in search queries.
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsStopWords>)]
|
||||
#[schema(value_type = Option<Vec<String>>, example = json!(["the", "a", "them", "their"]))]
|
||||
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")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsNonSeparatorTokens>)]
|
||||
#[schema(value_type = Option<Vec<String>>, example = json!([" ", "\n"]))]
|
||||
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")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSeparatorTokens>)]
|
||||
#[schema(value_type = Option<Vec<String>>, example = json!(["S"]))]
|
||||
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")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsDictionary>)]
|
||||
#[schema(value_type = Option<Vec<String>>, example = json!(["iPhone pro"]))]
|
||||
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")]
|
||||
#[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>>>,
|
||||
/// Search returns documents with distinct (different) values of the given field.
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsDistinctAttribute>)]
|
||||
#[schema(value_type = Option<String>, example = json!("sku"))]
|
||||
pub distinct_attribute: Setting<String>,
|
||||
/// Precision level when calculating the proximity ranking rule.
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsProximityPrecision>)]
|
||||
#[schema(value_type = Option<String>, example = json!(ProximityPrecisionView::ByAttribute))]
|
||||
pub proximity_precision: Setting<ProximityPrecisionView>,
|
||||
/// Customize typo tolerance feature.
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsTypoTolerance>)]
|
||||
#[schema(value_type = Option<TypoSettings>, example = json!({ "enabled": true, "disableOnAttributes": ["title"]}))]
|
||||
pub typo_tolerance: Setting<TypoSettings>,
|
||||
/// Faceting settings.
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsFaceting>)]
|
||||
#[schema(value_type = Option<FacetingSettings>, example = json!({ "maxValuesPerFacet": 10, "sortFacetValuesBy": { "genre": FacetValuesSort::Count }}))]
|
||||
pub faceting: Setting<FacetingSettings>,
|
||||
/// Pagination settings.
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsPagination>)]
|
||||
#[schema(value_type = Option<PaginationSettings>, example = json!({ "maxValuesPerFacet": 10, "sortFacetValuesBy": { "genre": FacetValuesSort::Count }}))]
|
||||
pub pagination: Setting<PaginationSettings>,
|
||||
|
||||
/// Embedder required for performing semantic search queries.
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsEmbedders>)]
|
||||
pub embedders: Setting<BTreeMap<String, Setting<milli::vector::settings::EmbeddingSettings>>>,
|
||||
#[schema(value_type = Option<BTreeMap<String, SettingEmbeddingSettings>>)]
|
||||
pub embedders: Setting<BTreeMap<String, SettingEmbeddingSettings>>,
|
||||
/// Maximum duration of a search query.
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSearchCutoffMs>)]
|
||||
#[schema(value_type = Option<u64>, example = json!(50))]
|
||||
pub search_cutoff_ms: Setting<u64>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsLocalizedAttributes>)]
|
||||
#[schema(value_type = Option<Vec<LocalizedAttributesRuleView>>, example = json!(50))]
|
||||
pub localized_attributes: Setting<Vec<LocalizedAttributesRuleView>>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsFacetSearch>)]
|
||||
#[schema(value_type = Option<bool>, example = json!(true))]
|
||||
pub facet_search: Setting<bool>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsPrefixSearch>)]
|
||||
#[schema(value_type = Option<PrefixSearchSettings>, example = json!("Hemlo"))]
|
||||
pub prefix_search: Setting<PrefixSearchSettings>,
|
||||
|
||||
#[serde(skip)]
|
||||
@ -221,7 +301,7 @@ impl<T> Settings<T> {
|
||||
};
|
||||
|
||||
for mut embedder in embedders.values_mut() {
|
||||
let Setting::Set(embedder) = &mut embedder else {
|
||||
let SettingEmbeddingSettings { inner: Setting::Set(embedder) } = &mut embedder else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@ -386,8 +466,9 @@ impl Settings<Unchecked> {
|
||||
let Setting::Set(mut configs) = self.embedders else { return Ok(self) };
|
||||
for (name, config) in configs.iter_mut() {
|
||||
let config_to_check = std::mem::take(config);
|
||||
let checked_config = milli::update::validate_embedding_settings(config_to_check, name)?;
|
||||
*config = checked_config
|
||||
let checked_config =
|
||||
milli::update::validate_embedding_settings(config_to_check.inner, name)?;
|
||||
*config = SettingEmbeddingSettings { inner: checked_config };
|
||||
}
|
||||
self.embedders = Setting::Set(configs);
|
||||
Ok(self)
|
||||
@ -665,7 +746,9 @@ pub fn apply_settings_to_builder(
|
||||
}
|
||||
|
||||
match embedders {
|
||||
Setting::Set(value) => builder.set_embedder_settings(value.clone()),
|
||||
Setting::Set(value) => builder.set_embedder_settings(
|
||||
value.iter().map(|(k, v)| (k.clone(), v.inner.clone())).collect(),
|
||||
),
|
||||
Setting::Reset => builder.reset_embedder_settings(),
|
||||
Setting::NotSet => (),
|
||||
}
|
||||
@ -779,7 +862,9 @@ pub fn settings(
|
||||
let embedders: BTreeMap<_, _> = index
|
||||
.embedding_configs(rtxn)?
|
||||
.into_iter()
|
||||
.map(|IndexEmbeddingConfig { name, config, .. }| (name, Setting::Set(config.into())))
|
||||
.map(|IndexEmbeddingConfig { name, config, .. }| {
|
||||
(name, SettingEmbeddingSettings { inner: Setting::Set(config.into()) })
|
||||
})
|
||||
.collect();
|
||||
let embedders = if embedders.is_empty() { Setting::NotSet } else { Setting::Set(embedders) };
|
||||
|
||||
@ -838,7 +923,7 @@ pub fn settings(
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserr)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserr, ToSchema)]
|
||||
#[deserr(try_from(&String) = FromStr::from_str -> CriterionError)]
|
||||
pub enum RankingRuleView {
|
||||
/// Sorted by decreasing number of matched query terms.
|
||||
@ -934,7 +1019,7 @@ impl From<RankingRuleView> for Criterion {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserr, Serialize, Deserialize)]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserr, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
#[deserr(error = DeserrJsonError<InvalidSettingsProximityPrecision>, rename_all = camelCase, deny_unknown_fields)]
|
||||
pub enum ProximityPrecisionView {
|
||||
@ -1001,8 +1086,9 @@ impl std::ops::Deref for WildcardSetting {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserr, Serialize, Deserialize)]
|
||||
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Deserr, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
#[deserr(error = DeserrJsonError<InvalidSettingsPrefixSearch>, rename_all = camelCase, deny_unknown_fields)]
|
||||
pub enum PrefixSearchSettings {
|
||||
#[default]
|
||||
|
@ -6,6 +6,7 @@ use std::str::FromStr;
|
||||
use deserr::{DeserializeError, Deserr, MergeWithError, ValueKind};
|
||||
use serde::de::Visitor;
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer};
|
||||
use utoipa::PartialSchema;
|
||||
|
||||
use crate::deserr::query_params::FromQueryParameter;
|
||||
|
||||
@ -229,7 +230,7 @@ pub enum OptionStarOrList<T> {
|
||||
List(Vec<T>),
|
||||
}
|
||||
|
||||
impl<T> OptionStarOrList<T> {
|
||||
impl<T: PartialSchema> OptionStarOrList<T> {
|
||||
pub fn is_some(&self) -> bool {
|
||||
match self {
|
||||
Self::None => false,
|
||||
|
@ -1,32 +1,49 @@
|
||||
use milli::Object;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::batches::BatchId;
|
||||
use crate::error::ResponseError;
|
||||
use crate::settings::{Settings, Unchecked};
|
||||
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")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct TaskView {
|
||||
/// The unique sequential identifier of the task.
|
||||
#[schema(value_type = u32, example = 4312)]
|
||||
pub uid: TaskId,
|
||||
/// The unique identifier of the index where this task is operated.
|
||||
#[schema(value_type = Option<u32>, example = json!("movies"))]
|
||||
pub batch_uid: Option<BatchId>,
|
||||
#[serde(default)]
|
||||
pub index_uid: Option<String>,
|
||||
pub status: Status,
|
||||
/// The type of the task.
|
||||
#[serde(rename = "type")]
|
||||
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>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub details: Option<DetailsView>,
|
||||
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)]
|
||||
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")]
|
||||
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)]
|
||||
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)]
|
||||
pub finished_at: Option<OffsetDateTime>,
|
||||
}
|
||||
@ -50,35 +67,48 @@ impl TaskView {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
#[derive(Default, Debug, PartialEq, Eq, Clone, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct DetailsView {
|
||||
/// Number of documents received for documentAdditionOrUpdate task.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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")]
|
||||
pub indexed_documents: Option<Option<u64>>,
|
||||
/// Number of documents edited for editDocumentByFunction task.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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")]
|
||||
pub primary_key: Option<Option<String>>,
|
||||
/// Number of provided document ids for the documentDeletion task.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub provided_ids: Option<usize>,
|
||||
/// Number of documents finally deleted for documentDeletion and indexDeletion tasks.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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")]
|
||||
pub matched_tasks: Option<u64>,
|
||||
/// Number of tasks canceled for taskCancelation.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub canceled_tasks: Option<Option<u64>>,
|
||||
/// Number of tasks deleted for taskDeletion.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub deleted_tasks: Option<Option<u64>>,
|
||||
/// Original filter query for taskCancelation or taskDeletion tasks.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub original_filter: Option<Option<String>>,
|
||||
/// Identifier generated for the dump for dumpCreation task.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub dump_uid: Option<Option<String>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub context: Option<Option<Object>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
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(flatten)]
|
||||
pub settings: Option<Box<Settings<Unchecked>>>,
|
||||
|
@ -9,6 +9,7 @@ use milli::Object;
|
||||
use roaring::RoaringBitmap;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
use time::{Duration, OffsetDateTime};
|
||||
use utoipa::ToSchema;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::batches::BatchId;
|
||||
@ -151,7 +152,7 @@ pub enum KindWithContent {
|
||||
SnapshotCreation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IndexSwap {
|
||||
pub indexes: (String, String),
|
||||
@ -363,9 +364,22 @@ impl From<&KindWithContent> for Option<Details> {
|
||||
}
|
||||
}
|
||||
|
||||
/// The status of a task.
|
||||
#[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")]
|
||||
pub enum Status {
|
||||
Enqueued,
|
||||
@ -424,10 +438,23 @@ impl fmt::Display for ParseTaskStatusError {
|
||||
}
|
||||
impl std::error::Error for ParseTaskStatusError {}
|
||||
|
||||
/// The type of the task.
|
||||
#[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")]
|
||||
#[schema(rename_all = "camelCase", example = json!(enum_iterator::all::<Kind>().collect::<Vec<_>>()))]
|
||||
pub enum Kind {
|
||||
DocumentAdditionOrUpdate,
|
||||
DocumentEdition,
|
||||
|
@ -105,6 +105,8 @@ tracing-actix-web = "0.7.11"
|
||||
build-info = { version = "1.7.0", path = "../build-info" }
|
||||
roaring = "0.10.7"
|
||||
mopa-maintained = "0.2.3"
|
||||
utoipa = { version = "5.2.0", features = ["actix_extras", "macros", "non_strict_integers", "preserve_order", "uuid", "time", "openapi_extensions"] }
|
||||
utoipa-scalar = { version = "0.2.0", optional = true, features = ["actix-web"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.10.0"
|
||||
@ -133,6 +135,7 @@ zip = { version = "2.1.3", optional = true }
|
||||
|
||||
[features]
|
||||
default = ["meilisearch-types/all-tokenizations", "mini-dashboard"]
|
||||
swagger = ["utoipa-scalar"]
|
||||
mini-dashboard = [
|
||||
"static-files",
|
||||
"anyhow",
|
||||
|
@ -13,14 +13,28 @@ use meilisearch_types::error::{Code, ResponseError};
|
||||
use meilisearch_types::keys::{CreateApiKey, Key, PatchApiKey};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use utoipa::{IntoParams, OpenApi, ToSchema};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::PAGINATION_DEFAULT_LIMIT;
|
||||
use super::{PaginationView, PAGINATION_DEFAULT_LIMIT, PAGINATION_DEFAULT_LIMIT_FN};
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::GuardedData;
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
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) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
@ -35,6 +49,51 @@ 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(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, Data<AuthController>>,
|
||||
body: AwebJson<CreateApiKey, DeserrJsonError>,
|
||||
@ -51,12 +110,15 @@ pub async fn create_api_key(
|
||||
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)]
|
||||
#[into_params(rename_all = "camelCase", parameter_in = Query)]
|
||||
pub struct ListApiKeys {
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidApiKeyOffset>)]
|
||||
#[param(value_type = usize, default = 0)]
|
||||
pub offset: Param<usize>,
|
||||
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidApiKeyLimit>)]
|
||||
#[param(value_type = usize, default = PAGINATION_DEFAULT_LIMIT_FN)]
|
||||
pub limit: Param<usize>,
|
||||
}
|
||||
|
||||
@ -66,6 +128,58 @@ impl ListApiKeys {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get API Keys
|
||||
///
|
||||
/// List all API Keys
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/",
|
||||
tag = "Keys",
|
||||
security(("Bearer" = ["keys.get", "keys.*", "*"])),
|
||||
params(ListApiKeys),
|
||||
responses(
|
||||
(status = 202, description = "List of keys", body = PaginationView<KeyView>, 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(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
|
||||
list_api_keys: AwebQueryParameter<ListApiKeys, DeserrQueryParamError>,
|
||||
@ -84,6 +198,51 @@ pub async fn list_api_keys(
|
||||
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 = "/{uidOrKey}",
|
||||
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(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
|
||||
path: web::Path<AuthParam>,
|
||||
@ -103,6 +262,53 @@ pub async fn get_api_key(
|
||||
Ok(HttpResponse::Ok().json(res))
|
||||
}
|
||||
|
||||
/// Update a Key
|
||||
///
|
||||
/// Update the name and description of an API key.
|
||||
/// Updates to keys are partial. This means you should provide only the fields you intend to update, as any fields not present in the payload will remain unchanged.
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/{uidOrKey}",
|
||||
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(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, Data<AuthController>>,
|
||||
body: AwebJson<PatchApiKey, DeserrJsonError>,
|
||||
@ -123,6 +329,35 @@ pub async fn patch_api_key(
|
||||
Ok(HttpResponse::Ok().json(res))
|
||||
}
|
||||
|
||||
/// Delete a key
|
||||
///
|
||||
/// Delete the specified API key.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/{uidOrKey}",
|
||||
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(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_DELETE }>, Data<AuthController>>,
|
||||
path: web::Path<AuthParam>,
|
||||
@ -144,19 +379,30 @@ pub struct AuthParam {
|
||||
key: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct KeyView {
|
||||
pub(super) struct KeyView {
|
||||
/// The name of the API Key if any
|
||||
name: Option<String>,
|
||||
/// The description of the API Key if any
|
||||
description: Option<String>,
|
||||
/// The actual API Key you can send to Meilisearch
|
||||
key: String,
|
||||
/// The `Uuid` specified while creating the key or autogenerated by Meilisearch.
|
||||
uid: Uuid,
|
||||
/// The actions accessible with this key.
|
||||
actions: Vec<Action>,
|
||||
/// The indexes accessible with this key.
|
||||
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")]
|
||||
expires_at: Option<OffsetDateTime>,
|
||||
/// The date of creation of this API Key.
|
||||
#[schema(read_only)]
|
||||
#[serde(serialize_with = "time::serde::rfc3339::serialize")]
|
||||
created_at: OffsetDateTime,
|
||||
/// The date of the last update made on this key.
|
||||
#[schema(read_only)]
|
||||
#[serde(serialize_with = "time::serde::rfc3339::serialize")]
|
||||
updated_at: OffsetDateTime,
|
||||
}
|
||||
|
@ -8,17 +8,76 @@ use meilisearch_types::deserr::DeserrQueryParamError;
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::keys::actions;
|
||||
use serde::Serialize;
|
||||
use utoipa::{OpenApi, ToSchema};
|
||||
|
||||
use super::tasks::TasksFilterQuery;
|
||||
use super::ActionPolicy;
|
||||
use crate::extractors::authentication::GuardedData;
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(get_batch, get_batches),
|
||||
tags((
|
||||
name = "Batches",
|
||||
description = "The /batches route gives information about the progress of batches of asynchronous operations.",
|
||||
external_docs(url = "https://www.meilisearch.com/docs/reference/api/batches"),
|
||||
)),
|
||||
)]
|
||||
pub struct BatchesApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::resource("").route(web::get().to(SeqHandler(get_batches))))
|
||||
.service(web::resource("/{batch_id}").route(web::get().to(SeqHandler(get_batch))));
|
||||
}
|
||||
|
||||
/// Get one batch
|
||||
///
|
||||
/// Get a single batch.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/{batchUid}",
|
||||
tag = "Batches",
|
||||
security(("Bearer" = ["tasks.get", "tasks.*", "*"])),
|
||||
params(
|
||||
("batchUid" = String, Path, example = "8685", description = "The unique batch id", nullable = false),
|
||||
),
|
||||
responses(
|
||||
(status = OK, description = "Return the batch", body = BatchView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"uid": 1,
|
||||
"details": {
|
||||
"receivedDocuments": 1,
|
||||
"indexedDocuments": 1
|
||||
},
|
||||
"progress": null,
|
||||
"stats": {
|
||||
"totalNbTasks": 1,
|
||||
"status": {
|
||||
"succeeded": 1
|
||||
},
|
||||
"types": {
|
||||
"documentAdditionOrUpdate": 1
|
||||
},
|
||||
"indexUids": {
|
||||
"INDEX_NAME": 1
|
||||
}
|
||||
},
|
||||
"duration": "PT0.364788S",
|
||||
"startedAt": "2024-12-10T15:48:49.672141Z",
|
||||
"finishedAt": "2024-12-10T15:48:50.036929Z"
|
||||
}
|
||||
)),
|
||||
(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_batch(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_GET }>, Data<IndexScheduler>>,
|
||||
batch_uid: web::Path<String>,
|
||||
@ -39,14 +98,14 @@ async fn get_batch(
|
||||
let (batches, _) = index_scheduler.get_batches_from_authorized_indexes(&query, filters)?;
|
||||
|
||||
if let Some(batch) = batches.first() {
|
||||
let task_view = BatchView::from_batch(batch);
|
||||
Ok(HttpResponse::Ok().json(task_view))
|
||||
let batch_view = BatchView::from_batch(batch);
|
||||
Ok(HttpResponse::Ok().json(batch_view))
|
||||
} else {
|
||||
Err(index_scheduler::Error::BatchNotFound(batch_uid).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct AllBatches {
|
||||
results: Vec<BatchView>,
|
||||
total: u64,
|
||||
@ -55,6 +114,63 @@ pub struct AllBatches {
|
||||
next: Option<u32>,
|
||||
}
|
||||
|
||||
/// Get batches
|
||||
///
|
||||
/// List all batches, regardless of index. The batch objects are contained in the results array.
|
||||
/// Batches are always returned in descending order of uid. This means that by default, the most recently created batch objects appear first.
|
||||
/// Batch results are paginated and can be filtered with query parameters.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/",
|
||||
tag = "Batches",
|
||||
security(("Bearer" = ["tasks.get", "tasks.*", "*"])),
|
||||
params(TasksFilterQuery),
|
||||
responses(
|
||||
(status = OK, description = "Return the batches", body = AllBatches, content_type = "application/json", example = json!(
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"uid": 2,
|
||||
"details": {
|
||||
"stopWords": [
|
||||
"of",
|
||||
"the"
|
||||
]
|
||||
},
|
||||
"progress": null,
|
||||
"stats": {
|
||||
"totalNbTasks": 1,
|
||||
"status": {
|
||||
"succeeded": 1
|
||||
},
|
||||
"types": {
|
||||
"settingsUpdate": 1
|
||||
},
|
||||
"indexUids": {
|
||||
"INDEX_NAME": 1
|
||||
}
|
||||
},
|
||||
"duration": "PT0.110083S",
|
||||
"startedAt": "2024-12-10T15:49:04.995321Z",
|
||||
"finishedAt": "2024-12-10T15:49:05.105404Z"
|
||||
}
|
||||
],
|
||||
"total": 3,
|
||||
"limit": 1,
|
||||
"from": 2,
|
||||
"next": 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"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
async fn get_batches(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_GET }>, Data<IndexScheduler>>,
|
||||
params: AwebQueryParameter<TasksFilterQuery, DeserrQueryParamError>,
|
||||
|
@ -5,6 +5,7 @@ use meilisearch_auth::AuthController;
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::tasks::KindWithContent;
|
||||
use tracing::debug;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use crate::analytics::Analytics;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
@ -13,12 +14,60 @@ use crate::extractors::sequential_extractor::SeqHandler;
|
||||
use crate::routes::{get_task_id, is_dry_run, SummarizedTaskView};
|
||||
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
|
||||
exported—together with their documents and settings—and 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) {
|
||||
cfg.service(web::resource("").route(web::post().to(SeqHandler(create_dump))));
|
||||
}
|
||||
|
||||
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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DUMPS_CREATE }>, Data<IndexScheduler>>,
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::DUMPS_CREATE }>, Data<AuthController>>,
|
||||
|
@ -8,12 +8,26 @@ use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::keys::actions;
|
||||
use serde::Serialize;
|
||||
use tracing::debug;
|
||||
use utoipa::{OpenApi, ToSchema};
|
||||
|
||||
use crate::analytics::{Aggregate, Analytics};
|
||||
use crate::extractors::authentication::policies::ActionPolicy;
|
||||
use crate::extractors::authentication::GuardedData;
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(get_features, patch_features),
|
||||
tags((
|
||||
name = "Experimental features",
|
||||
description = "The `/experimental-features` route allows you to activate or deactivate some of Meilisearch's experimental features.
|
||||
|
||||
This route is **synchronous**. This means that no task object will be returned, and any activated or deactivated features will be made available or unavailable immediately.",
|
||||
external_docs(url = "https://www.meilisearch.com/docs/reference/api/experimental_features"),
|
||||
)),
|
||||
)]
|
||||
pub struct ExperimentalFeaturesApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
@ -22,6 +36,32 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Get all experimental features
|
||||
///
|
||||
/// Get a list of all experimental features that can be activated via the /experimental-features route and whether or not they are currently activated.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/",
|
||||
tag = "Experimental features",
|
||||
security(("Bearer" = ["experimental_features.get", "experimental_features.*", "*"])),
|
||||
responses(
|
||||
(status = OK, description = "Experimental features are returned", body = RuntimeTogglableFeatures, content_type = "application/json", example = json!(
|
||||
{
|
||||
"metrics": false,
|
||||
"logsRoute": true,
|
||||
"vectorSearch": false,
|
||||
}
|
||||
)),
|
||||
(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_features(
|
||||
index_scheduler: GuardedData<
|
||||
ActionPolicy<{ actions::EXPERIMENTAL_FEATURES_GET }>,
|
||||
@ -35,8 +75,9 @@ async fn get_features(
|
||||
HttpResponse::Ok().json(features)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserr)]
|
||||
#[derive(Debug, Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct RuntimeTogglableFeatures {
|
||||
#[deserr(default)]
|
||||
pub vector_store: Option<bool>,
|
||||
@ -79,6 +120,32 @@ impl Aggregate for PatchExperimentalFeatureAnalytics {
|
||||
}
|
||||
}
|
||||
|
||||
/// Configure experimental features
|
||||
///
|
||||
/// Activate or deactivate experimental features.
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/",
|
||||
tag = "Experimental features",
|
||||
security(("Bearer" = ["experimental_features.update", "experimental_features.*", "*"])),
|
||||
responses(
|
||||
(status = OK, description = "Experimental features are returned", body = RuntimeTogglableFeatures, content_type = "application/json", example = json!(
|
||||
{
|
||||
"metrics": false,
|
||||
"logsRoute": true,
|
||||
"vectorSearch": false,
|
||||
}
|
||||
)),
|
||||
(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 patch_features(
|
||||
index_scheduler: GuardedData<
|
||||
ActionPolicy<{ actions::EXPERIMENTAL_FEATURES_UPDATE }>,
|
||||
|
@ -31,6 +31,7 @@ use tempfile::tempfile;
|
||||
use tokio::fs::File;
|
||||
use tokio::io::{AsyncSeekExt, AsyncWriteExt, BufWriter};
|
||||
use tracing::debug;
|
||||
use utoipa::{IntoParams, OpenApi, ToSchema};
|
||||
|
||||
use crate::analytics::{Aggregate, AggregateMethod, Analytics};
|
||||
use crate::error::MeilisearchHttpError;
|
||||
@ -71,6 +72,19 @@ pub struct DocumentParam {
|
||||
document_id: String,
|
||||
}
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(get_document, get_documents, delete_document, replace_documents, update_documents, clear_all_documents, delete_documents_batch, delete_documents_by_filter, edit_documents_by_function, documents_by_query_post),
|
||||
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) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
@ -93,12 +107,18 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserr)]
|
||||
#[derive(Debug, Deserr, IntoParams, ToSchema)]
|
||||
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[into_params(rename_all = "camelCase", parameter_in = Query)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct GetDocument {
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentFields>)]
|
||||
#[param(value_type = Option<Vec<String>>)]
|
||||
#[schema(value_type = Option<Vec<String>>)]
|
||||
fields: OptionStarOrList<String>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentRetrieveVectors>)]
|
||||
#[param(value_type = Option<bool>)]
|
||||
#[schema(value_type = Option<bool>)]
|
||||
retrieve_vectors: Param<bool>,
|
||||
}
|
||||
|
||||
@ -174,6 +194,55 @@ impl<Method: AggregateMethod> Aggregate for DocumentsFetchAggregator<Method> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get one document
|
||||
///
|
||||
/// Get one document from its primary key.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "{indexUid}/documents/{documentId}",
|
||||
tag = "Documents",
|
||||
security(("Bearer" = ["documents.get", "documents.*", "*"])),
|
||||
params(
|
||||
("indexUid" = String, Path, example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
("documentId" = String, Path, example = "85087", description = "The document identifier", nullable = false),
|
||||
GetDocument,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "The document is returned", body = serde_json::Value, content_type = "application/json", example = json!(
|
||||
{
|
||||
"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
|
||||
}
|
||||
)),
|
||||
(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 = 404, description = "Document not found", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Document `a` not found.",
|
||||
"code": "document_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#document_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_document(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>,
|
||||
document_param: web::Path<DocumentParam>,
|
||||
@ -237,6 +306,38 @@ impl Aggregate for DocumentsDeletionAggregator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete a document
|
||||
///
|
||||
/// Delete a single document by id.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "{indexUid}/documents/{documentId}",
|
||||
tag = "Documents",
|
||||
security(("Bearer" = ["documents.delete", "documents.*", "*"])),
|
||||
params(
|
||||
("indexUid" = String, Path, example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
("documentId" = String, Path, example = "853", description = "Document Identifier", nullable = false),
|
||||
),
|
||||
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_document(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>,
|
||||
path: web::Path<DocumentParam>,
|
||||
@ -271,36 +372,98 @@ pub async fn delete_document(
|
||||
Ok(HttpResponse::Accepted().json(task))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserr)]
|
||||
#[derive(Debug, Deserr, IntoParams)]
|
||||
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[into_params(rename_all = "camelCase", parameter_in = Query)]
|
||||
pub struct BrowseQueryGet {
|
||||
#[param(default, value_type = Option<usize>)]
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentOffset>)]
|
||||
offset: Param<usize>,
|
||||
#[param(default, value_type = Option<usize>)]
|
||||
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidDocumentLimit>)]
|
||||
limit: Param<usize>,
|
||||
#[param(default, value_type = Option<Vec<String>>)]
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentFields>)]
|
||||
fields: OptionStarOrList<String>,
|
||||
#[param(default, value_type = Option<bool>)]
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentRetrieveVectors>)]
|
||||
retrieve_vectors: Param<bool>,
|
||||
#[param(default, value_type = Option<String>, example = "popularity > 1000")]
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidDocumentFilter>)]
|
||||
filter: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserr)]
|
||||
#[derive(Debug, Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct BrowseQuery {
|
||||
#[schema(default, example = 150)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidDocumentOffset>)]
|
||||
offset: usize,
|
||||
#[schema(default = 20, example = 1)]
|
||||
#[deserr(default = PAGINATION_DEFAULT_LIMIT, error = DeserrJsonError<InvalidDocumentLimit>)]
|
||||
limit: usize,
|
||||
#[schema(example = json!(["title, description"]))]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidDocumentFields>)]
|
||||
fields: Option<Vec<String>>,
|
||||
#[schema(default, example = true)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidDocumentRetrieveVectors>)]
|
||||
retrieve_vectors: bool,
|
||||
#[schema(default, value_type = Option<Value>, example = "popularity > 1000")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidDocumentFilter>)]
|
||||
filter: Option<Value>,
|
||||
}
|
||||
|
||||
/// Get documents with POST
|
||||
///
|
||||
/// Get a set of documents.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "{indexUid}/documents/fetch",
|
||||
tag = "Documents",
|
||||
security(("Bearer" = ["documents.delete", "documents.*", "*"])),
|
||||
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
request_body = BrowseQuery,
|
||||
responses(
|
||||
(status = 200, description = "Task successfully enqueued", body = PaginationView<serde_json::Value>, content_type = "application/json", example = json!(
|
||||
{
|
||||
"results":[
|
||||
{
|
||||
"title":"The Travels of Ibn Battuta",
|
||||
"genres":[
|
||||
"Travel",
|
||||
"Adventure"
|
||||
],
|
||||
"language":"English",
|
||||
"rating":4.5
|
||||
},
|
||||
{
|
||||
"title":"Pride and Prejudice",
|
||||
"genres":[
|
||||
"Classics",
|
||||
"Fiction",
|
||||
"Romance",
|
||||
"Literature"
|
||||
],
|
||||
"language":"English",
|
||||
"rating":4
|
||||
},
|
||||
],
|
||||
"offset":0,
|
||||
"limit":2,
|
||||
"total":5
|
||||
}
|
||||
)),
|
||||
(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 documents_by_query_post(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -326,6 +489,60 @@ pub async fn documents_by_query_post(
|
||||
documents_by_query(&index_scheduler, index_uid, body)
|
||||
}
|
||||
|
||||
/// Get documents
|
||||
///
|
||||
/// Get documents by batches.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "{indexUid}/documents",
|
||||
tag = "Documents",
|
||||
security(("Bearer" = ["documents.get", "documents.*", "*"])),
|
||||
params(
|
||||
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
BrowseQueryGet
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "The documents are returned", body = PaginationView<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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -396,11 +613,17 @@ fn documents_by_query(
|
||||
Ok(HttpResponse::Ok().json(ret))
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug, Deserr)]
|
||||
#[derive(Deserialize, Debug, Deserr, IntoParams)]
|
||||
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[into_params(parameter_in = Query, rename_all = "camelCase")]
|
||||
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>)]
|
||||
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>)]
|
||||
pub csv_delimiter: Option<u8>,
|
||||
}
|
||||
@ -451,6 +674,52 @@ 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",
|
||||
tag = "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,
|
||||
),
|
||||
request_body = serde_json::Value,
|
||||
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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -508,6 +777,50 @@ pub async fn replace_documents(
|
||||
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",
|
||||
tag = "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,
|
||||
),
|
||||
request_body = serde_json::Value,
|
||||
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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -742,6 +1055,38 @@ async fn copy_body_to_file(
|
||||
Ok(read_file)
|
||||
}
|
||||
|
||||
/// Delete documents by batch
|
||||
///
|
||||
/// Delete a set of documents based on an array of document ids.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "{indexUid}/delete-batch",
|
||||
tag = "Documents",
|
||||
security(("Bearer" = ["documents.delete", "documents.*", "*"])),
|
||||
params(
|
||||
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
),
|
||||
request_body = Vec<Value>,
|
||||
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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -781,13 +1126,44 @@ pub async fn delete_documents_batch(
|
||||
Ok(HttpResponse::Accepted().json(task))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserr)]
|
||||
#[derive(Debug, Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct DocumentDeletionByFilter {
|
||||
#[deserr(error = DeserrJsonError<InvalidDocumentFilter>, missing_field_error = DeserrJsonError::missing_document_filter)]
|
||||
filter: Value,
|
||||
}
|
||||
|
||||
/// Delete documents by filter
|
||||
///
|
||||
/// Delete a set of documents based on a filter.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "{indexUid}/documents/delete",
|
||||
tag = "Documents",
|
||||
security(("Bearer" = ["documents.delete", "documents.*", "*"])),
|
||||
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
request_body = DocumentDeletionByFilter,
|
||||
responses(
|
||||
(status = ACCEPTED, 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 delete_documents_by_filter(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -828,13 +1204,16 @@ pub async fn delete_documents_by_filter(
|
||||
Ok(HttpResponse::Accepted().json(task))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserr)]
|
||||
#[derive(Debug, Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct DocumentEditionByFunction {
|
||||
/// A string containing a RHAI function.
|
||||
#[deserr(default, error = DeserrJsonError<InvalidDocumentFilter>)]
|
||||
pub filter: Option<Value>,
|
||||
/// A string containing a filter expression.
|
||||
#[deserr(default, error = DeserrJsonError<InvalidDocumentEditionContext>)]
|
||||
pub context: Option<Value>,
|
||||
/// An object with data Meilisearch should make available for the editing function.
|
||||
#[deserr(error = DeserrJsonError<InvalidDocumentEditionFunctionFilter>, missing_field_error = DeserrJsonError::missing_document_edition_function)]
|
||||
pub function: String,
|
||||
}
|
||||
@ -867,6 +1246,38 @@ impl Aggregate for EditDocumentsByFunctionAggregator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Edit documents by function.
|
||||
///
|
||||
/// Use a [RHAI function](https://rhai.rs/book/engine/hello-world.html) to edit one or more documents directly in Meilisearch.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "{indexUid}/documents/edit",
|
||||
tag = "Documents",
|
||||
security(("Bearer" = ["documents.*", "*"])),
|
||||
params(
|
||||
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
),
|
||||
request_body = DocumentEditionByFunction,
|
||||
responses(
|
||||
(status = 202, 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 edit_documents_by_function(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ALL }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -936,6 +1347,35 @@ pub async fn edit_documents_by_function(
|
||||
Ok(HttpResponse::Accepted().json(task))
|
||||
}
|
||||
|
||||
/// Delete all documents
|
||||
///
|
||||
/// Delete all documents in the specified index.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "{indexUid}/documents",
|
||||
tag = "Documents",
|
||||
security(("Bearer" = ["documents.delete", "documents.*", "*"])),
|
||||
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
|
@ -11,6 +11,7 @@ use meilisearch_types::index_uid::IndexUid;
|
||||
use meilisearch_types::locales::Locale;
|
||||
use serde_json::Value;
|
||||
use tracing::debug;
|
||||
use utoipa::{OpenApi, ToSchema};
|
||||
|
||||
use crate::analytics::{Aggregate, Analytics};
|
||||
use crate::extractors::authentication::policies::*;
|
||||
@ -18,20 +19,33 @@ use crate::extractors::authentication::GuardedData;
|
||||
use crate::routes::indexes::search::search_kind;
|
||||
use crate::search::{
|
||||
add_search_rules, perform_facet_search, FacetSearchResult, HybridQuery, MatchingStrategy,
|
||||
RankingScoreThreshold, SearchQuery, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER,
|
||||
RankingScoreThreshold, SearchQuery, SearchResult, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER,
|
||||
DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT,
|
||||
DEFAULT_SEARCH_OFFSET,
|
||||
};
|
||||
use crate::search_queue::SearchQueue;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(search),
|
||||
tags(
|
||||
(
|
||||
name = "Facet Search",
|
||||
description = "The `/facet-search` route allows you to search for facet values. Facet search supports prefix search and typo tolerance. The returned hits are sorted lexicographically in ascending order. You can configure how facets are sorted using the sortFacetValuesBy property of the faceting index settings.",
|
||||
external_docs(url = "https://www.meilisearch.com/docs/reference/api/facet_search"),
|
||||
),
|
||||
),
|
||||
)]
|
||||
pub struct FacetSearchApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::resource("").route(web::post().to(search)));
|
||||
}
|
||||
|
||||
/// # Important
|
||||
///
|
||||
/// Intentionally don't use `deny_unknown_fields` to ignore search parameters sent by user
|
||||
#[derive(Debug, Clone, Default, PartialEq, deserr::Deserr)]
|
||||
// # Important
|
||||
//
|
||||
// Intentionally don't use `deny_unknown_fields` to ignore search parameters sent by user
|
||||
#[derive(Debug, Clone, Default, PartialEq, deserr::Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase)]
|
||||
pub struct FacetSearchQuery {
|
||||
#[deserr(default, error = DeserrJsonError<InvalidFacetSearchQuery>)]
|
||||
@ -158,6 +172,60 @@ impl Aggregate for FacetSearchAggregator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform a facet search
|
||||
///
|
||||
/// Search for a facet value within a given facet.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "{indexUid}/facet-search",
|
||||
tag = "Facet Search",
|
||||
security(("Bearer" = ["search", "*"])),
|
||||
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
request_body = FacetSearchQuery,
|
||||
responses(
|
||||
(status = 200, description = "The documents are returned", body = SearchResult, content_type = "application/json", example = json!(
|
||||
{
|
||||
"hits": [
|
||||
{
|
||||
"id": 2770,
|
||||
"title": "American Pie 2",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/q4LNgUnRfltxzp3gf1MAGiK5LhV.jpg",
|
||||
"overview": "The whole gang are back and as close as ever. They decide to get even closer by spending the summer together at a beach house. They decide to hold the biggest…",
|
||||
"release_date": 997405200
|
||||
},
|
||||
{
|
||||
"id": 190859,
|
||||
"title": "American Sniper",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/svPHnYE7N5NAGO49dBmRhq0vDQ3.jpg",
|
||||
"overview": "U.S. Navy SEAL Chris Kyle takes his sole mission—protect his comrades—to heart and becomes one of the most lethal snipers in American history. His pinpoint accuracy not only saves countless lives but also makes him a prime…",
|
||||
"release_date": 1418256000
|
||||
}
|
||||
],
|
||||
"offset": 0,
|
||||
"limit": 2,
|
||||
"estimatedTotalHits": 976,
|
||||
"processingTimeMs": 35,
|
||||
"query": "american "
|
||||
}
|
||||
)),
|
||||
(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 search(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>,
|
||||
search_queue: Data<SearchQueue>,
|
||||
|
@ -16,8 +16,11 @@ use meilisearch_types::tasks::KindWithContent;
|
||||
use serde::Serialize;
|
||||
use time::OffsetDateTime;
|
||||
use tracing::debug;
|
||||
use utoipa::{IntoParams, OpenApi, ToSchema};
|
||||
|
||||
use super::{get_task_id, Pagination, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT};
|
||||
use super::{
|
||||
get_task_id, Pagination, PaginationView, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT,
|
||||
};
|
||||
use crate::analytics::{Aggregate, Analytics};
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::{AuthenticationError, GuardedData};
|
||||
@ -36,6 +39,25 @@ mod settings_analytics;
|
||||
pub mod similar;
|
||||
mod similar_analytics;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
nest(
|
||||
(path = "/", api = documents::DocumentsApi),
|
||||
(path = "/", api = facet_search::FacetSearchApi),
|
||||
(path = "/", api = similar::SimilarApi),
|
||||
(path = "/", api = settings::SettingsApi),
|
||||
),
|
||||
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) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
@ -59,14 +81,18 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[derive(Debug, Serialize, Clone, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IndexView {
|
||||
/// Unique identifier for the index
|
||||
pub uid: String,
|
||||
/// An `RFC 3339` format for date/time/duration.
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: OffsetDateTime,
|
||||
/// An `RFC 3339` format for date/time/duration.
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated_at: OffsetDateTime,
|
||||
/// Custom primaryKey for documents
|
||||
pub primary_key: Option<String>,
|
||||
}
|
||||
|
||||
@ -84,20 +110,61 @@ impl IndexView {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserr, Debug, Clone, Copy)]
|
||||
#[derive(Deserr, Debug, Clone, Copy, IntoParams)]
|
||||
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[into_params(rename_all = "camelCase", parameter_in = Query)]
|
||||
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>)]
|
||||
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>)]
|
||||
pub limit: Param<usize>,
|
||||
}
|
||||
|
||||
impl ListIndexes {
|
||||
fn as_pagination(self) -> Pagination {
|
||||
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 = PaginationView<IndexView>, 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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>,
|
||||
paginate: AwebQueryParameter<ListIndexes, DeserrQueryParamError>,
|
||||
@ -121,11 +188,16 @@ pub async fn list_indexes(
|
||||
Ok(HttpResponse::Ok().json(ret))
|
||||
}
|
||||
|
||||
#[derive(Deserr, Debug)]
|
||||
#[derive(Deserr, Debug, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct IndexCreateRequest {
|
||||
/// The name of the index
|
||||
#[schema(example = "movies")]
|
||||
#[deserr(error = DeserrJsonError<InvalidIndexUid>, missing_field_error = DeserrJsonError::missing_index_uid)]
|
||||
uid: IndexUid,
|
||||
/// The primary key of the index
|
||||
#[schema(example = "id")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)]
|
||||
primary_key: Option<String>,
|
||||
}
|
||||
@ -149,6 +221,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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_CREATE }>, Data<IndexScheduler>>,
|
||||
body: AwebJson<IndexCreateRequest, DeserrJsonError>,
|
||||
@ -198,13 +299,42 @@ fn deny_immutable_fields_index(
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserr, Debug)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields = deny_immutable_fields_index)]
|
||||
pub struct UpdateIndexRequest {
|
||||
#[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)]
|
||||
primary_key: Option<String>,
|
||||
}
|
||||
|
||||
/// Get index
|
||||
///
|
||||
/// Get information about an index.
|
||||
#[utoipa::path(
|
||||
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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -237,6 +367,47 @@ impl Aggregate for IndexUpdatedAggregate {
|
||||
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"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn update_index(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_UPDATE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -269,6 +440,35 @@ pub async fn update_index(
|
||||
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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_DELETE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -289,14 +489,15 @@ pub async fn delete_index(
|
||||
}
|
||||
|
||||
/// Stats of an `Index`, as known to the `stats` route.
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize, Debug, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct IndexStats {
|
||||
/// Number of documents in the index
|
||||
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,
|
||||
/// 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,
|
||||
}
|
||||
|
||||
@ -310,6 +511,44 @@ impl From<index_scheduler::IndexStats> for IndexStats {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get stats of index
|
||||
///
|
||||
/// Get the stats of an index.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/{indexUid}/stats",
|
||||
tag = "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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
|
@ -12,6 +12,7 @@ use meilisearch_types::milli;
|
||||
use meilisearch_types::serde_cs::vec::CS;
|
||||
use serde_json::Value;
|
||||
use tracing::debug;
|
||||
use utoipa::{IntoParams, OpenApi};
|
||||
|
||||
use crate::analytics::Analytics;
|
||||
use crate::error::MeilisearchHttpError;
|
||||
@ -22,12 +23,28 @@ use crate::metrics::MEILISEARCH_DEGRADED_SEARCH_REQUESTS;
|
||||
use crate::routes::indexes::search_analytics::{SearchAggregator, SearchGET, SearchPOST};
|
||||
use crate::search::{
|
||||
add_search_rules, perform_search, HybridQuery, MatchingStrategy, RankingScoreThreshold,
|
||||
RetrieveVectors, SearchKind, SearchQuery, SemanticRatio, DEFAULT_CROP_LENGTH,
|
||||
RetrieveVectors, SearchKind, SearchQuery, SearchResult, SemanticRatio, DEFAULT_CROP_LENGTH,
|
||||
DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG,
|
||||
DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, DEFAULT_SEMANTIC_RATIO,
|
||||
};
|
||||
use crate::search_queue::SearchQueue;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(search_with_url_query, search_with_post),
|
||||
tags(
|
||||
(
|
||||
name = "Search",
|
||||
description = "Meilisearch exposes two routes to perform searches:
|
||||
|
||||
- A POST route: this is the preferred route when using API authentication, as it allows [preflight request](https://developer.mozilla.org/en-US/docs/Glossary/Preflight_request) caching and better performance.
|
||||
- A GET route: the usage of this route is discouraged, unless you have good reason to do otherwise (specific caching abilities for example)",
|
||||
external_docs(url = "https://www.meilisearch.com/docs/reference/api/search"),
|
||||
),
|
||||
),
|
||||
)]
|
||||
pub struct SearchApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
@ -36,30 +53,41 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, deserr::Deserr)]
|
||||
#[derive(Debug, deserr::Deserr, IntoParams)]
|
||||
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[into_params(rename_all = "camelCase", parameter_in = Query)]
|
||||
pub struct SearchQueryGet {
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchQ>)]
|
||||
q: Option<String>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchVector>)]
|
||||
#[param(value_type = Vec<f32>, explode = false)]
|
||||
vector: Option<CS<f32>>,
|
||||
#[deserr(default = Param(DEFAULT_SEARCH_OFFSET()), error = DeserrQueryParamError<InvalidSearchOffset>)]
|
||||
#[param(value_type = usize, default = DEFAULT_SEARCH_OFFSET)]
|
||||
offset: Param<usize>,
|
||||
#[deserr(default = Param(DEFAULT_SEARCH_LIMIT()), error = DeserrQueryParamError<InvalidSearchLimit>)]
|
||||
#[param(value_type = usize, default = DEFAULT_SEARCH_LIMIT)]
|
||||
limit: Param<usize>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchPage>)]
|
||||
#[param(value_type = Option<usize>)]
|
||||
page: Option<Param<usize>>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchHitsPerPage>)]
|
||||
#[param(value_type = Option<usize>)]
|
||||
hits_per_page: Option<Param<usize>>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchAttributesToRetrieve>)]
|
||||
#[param(value_type = Vec<String>, explode = false)]
|
||||
attributes_to_retrieve: Option<CS<String>>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchRetrieveVectors>)]
|
||||
#[param(value_type = bool, default)]
|
||||
retrieve_vectors: Param<bool>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchAttributesToCrop>)]
|
||||
#[param(value_type = Vec<String>, explode = false)]
|
||||
attributes_to_crop: Option<CS<String>>,
|
||||
#[deserr(default = Param(DEFAULT_CROP_LENGTH()), error = DeserrQueryParamError<InvalidSearchCropLength>)]
|
||||
#[param(value_type = usize, default = DEFAULT_CROP_LENGTH)]
|
||||
crop_length: Param<usize>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchAttributesToHighlight>)]
|
||||
#[param(value_type = Vec<String>, explode = false)]
|
||||
attributes_to_highlight: Option<CS<String>>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchFilter>)]
|
||||
filter: Option<String>,
|
||||
@ -68,30 +96,41 @@ pub struct SearchQueryGet {
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchDistinct>)]
|
||||
distinct: Option<String>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchShowMatchesPosition>)]
|
||||
#[param(value_type = bool)]
|
||||
show_matches_position: Param<bool>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchShowRankingScore>)]
|
||||
#[param(value_type = bool)]
|
||||
show_ranking_score: Param<bool>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchShowRankingScoreDetails>)]
|
||||
#[param(value_type = bool)]
|
||||
show_ranking_score_details: Param<bool>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchFacets>)]
|
||||
#[param(value_type = Vec<String>, explode = false)]
|
||||
facets: Option<CS<String>>,
|
||||
#[deserr( default = DEFAULT_HIGHLIGHT_PRE_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPreTag>)]
|
||||
#[deserr(default = DEFAULT_HIGHLIGHT_PRE_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPreTag>)]
|
||||
#[param(default = DEFAULT_HIGHLIGHT_PRE_TAG)]
|
||||
highlight_pre_tag: String,
|
||||
#[deserr( default = DEFAULT_HIGHLIGHT_POST_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPostTag>)]
|
||||
#[deserr(default = DEFAULT_HIGHLIGHT_POST_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPostTag>)]
|
||||
#[param(default = DEFAULT_HIGHLIGHT_POST_TAG)]
|
||||
highlight_post_tag: String,
|
||||
#[deserr(default = DEFAULT_CROP_MARKER(), error = DeserrQueryParamError<InvalidSearchCropMarker>)]
|
||||
#[param(default = DEFAULT_CROP_MARKER)]
|
||||
crop_marker: String,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchMatchingStrategy>)]
|
||||
matching_strategy: MatchingStrategy,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchAttributesToSearchOn>)]
|
||||
#[param(value_type = Vec<String>, explode = false)]
|
||||
pub attributes_to_search_on: Option<CS<String>>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidEmbedder>)]
|
||||
pub hybrid_embedder: Option<String>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchSemanticRatio>)]
|
||||
#[param(value_type = f32)]
|
||||
pub hybrid_semantic_ratio: Option<SemanticRatioGet>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchRankingScoreThreshold>)]
|
||||
#[param(value_type = f32)]
|
||||
pub ranking_score_threshold: Option<RankingScoreThresholdGet>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchLocales>)]
|
||||
#[param(value_type = Vec<Locale>, explode = false)]
|
||||
pub locales: Option<CS<Locale>>,
|
||||
}
|
||||
|
||||
@ -220,6 +259,62 @@ pub fn fix_sort_query_parameters(sort_query: &str) -> Vec<String> {
|
||||
sort_parameters
|
||||
}
|
||||
|
||||
/// Search an index with GET
|
||||
///
|
||||
/// Search for documents matching a specific query in the given index.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/{indexUid}/search",
|
||||
tags = ["Indexes", "Search"],
|
||||
security(("Bearer" = ["search", "*"])),
|
||||
params(
|
||||
("indexUid" = String, Path, example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
SearchQueryGet
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "The documents are returned", body = SearchResult, content_type = "application/json", example = json!(
|
||||
{
|
||||
"hits": [
|
||||
{
|
||||
"id": 2770,
|
||||
"title": "American Pie 2",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/q4LNgUnRfltxzp3gf1MAGiK5LhV.jpg",
|
||||
"overview": "The whole gang are back and as close as ever. They decide to get even closer by spending the summer together at a beach house. They decide to hold the biggest…",
|
||||
"release_date": 997405200
|
||||
},
|
||||
{
|
||||
"id": 190859,
|
||||
"title": "American Sniper",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/svPHnYE7N5NAGO49dBmRhq0vDQ3.jpg",
|
||||
"overview": "U.S. Navy SEAL Chris Kyle takes his sole mission—protect his comrades—to heart and becomes one of the most lethal snipers in American history. His pinpoint accuracy not only saves countless lives but also makes him a prime…",
|
||||
"release_date": 1418256000
|
||||
}
|
||||
],
|
||||
"offset": 0,
|
||||
"limit": 2,
|
||||
"estimatedTotalHits": 976,
|
||||
"processingTimeMs": 35,
|
||||
"query": "american "
|
||||
}
|
||||
)),
|
||||
(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 search_with_url_query(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>,
|
||||
search_queue: web::Data<SearchQueue>,
|
||||
@ -271,6 +366,62 @@ pub async fn search_with_url_query(
|
||||
Ok(HttpResponse::Ok().json(search_result))
|
||||
}
|
||||
|
||||
/// Search with POST
|
||||
///
|
||||
/// Search for documents matching a specific query in the given index.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/{indexUid}/search",
|
||||
tags = ["Indexes", "Search"],
|
||||
security(("Bearer" = ["search", "*"])),
|
||||
params(
|
||||
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
),
|
||||
request_body = SearchQuery,
|
||||
responses(
|
||||
(status = 200, description = "The documents are returned", body = SearchResult, content_type = "application/json", example = json!(
|
||||
{
|
||||
"hits": [
|
||||
{
|
||||
"id": 2770,
|
||||
"title": "American Pie 2",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/q4LNgUnRfltxzp3gf1MAGiK5LhV.jpg",
|
||||
"overview": "The whole gang are back and as close as ever. They decide to get even closer by spending the summer together at a beach house. They decide to hold the biggest…",
|
||||
"release_date": 997405200
|
||||
},
|
||||
{
|
||||
"id": 190859,
|
||||
"title": "American Sniper",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/svPHnYE7N5NAGO49dBmRhq0vDQ3.jpg",
|
||||
"overview": "U.S. Navy SEAL Chris Kyle takes his sole mission—protect his comrades—to heart and becomes one of the most lethal snipers in American history. His pinpoint accuracy not only saves countless lives but also makes him a prime…",
|
||||
"release_date": 1418256000
|
||||
}
|
||||
],
|
||||
"offset": 0,
|
||||
"limit": 2,
|
||||
"estimatedTotalHits": 976,
|
||||
"processingTimeMs": 35,
|
||||
"query": "american "
|
||||
}
|
||||
)),
|
||||
(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 search_with_post(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>,
|
||||
search_queue: web::Data<SearchQueue>,
|
||||
|
@ -6,9 +6,12 @@ use meilisearch_types::deserr::DeserrJsonError;
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::index_uid::IndexUid;
|
||||
use meilisearch_types::milli::update::Setting;
|
||||
use meilisearch_types::settings::{settings, SecretPolicy, Settings, Unchecked};
|
||||
use meilisearch_types::settings::{
|
||||
settings, SecretPolicy, SettingEmbeddingSettings, Settings, Unchecked,
|
||||
};
|
||||
use meilisearch_types::tasks::KindWithContent;
|
||||
use tracing::debug;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use super::settings_analytics::*;
|
||||
use crate::analytics::Analytics;
|
||||
@ -29,6 +32,19 @@ macro_rules! make_setting_routes {
|
||||
make_setting_route!($route, $update_verb, $type, $err_ty, $attr, $camelcase_attr, $analytics);
|
||||
)*
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(update_all, get_all, delete_all, $( $attr::get, $attr::update, $attr::delete,)*),
|
||||
tags(
|
||||
(
|
||||
name = "Settings",
|
||||
description = "Use the /settings route to customize search settings for a given index. You can either modify all index settings at once using the update settings endpoint, or use a child route to configure a single setting.",
|
||||
external_docs(url = "https://www.meilisearch.com/docs/reference/api/settings"),
|
||||
),
|
||||
),
|
||||
)]
|
||||
pub struct SettingsApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
cfg.service(
|
||||
@ -62,7 +78,38 @@ macro_rules! make_setting_route {
|
||||
use $crate::extractors::sequential_extractor::SeqHandler;
|
||||
use $crate::Opt;
|
||||
use $crate::routes::{is_dry_run, get_task_id, SummarizedTaskView};
|
||||
#[allow(unused_imports)]
|
||||
use super::*;
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = concat!("{indexUid}/settings", $route),
|
||||
tag = "Settings",
|
||||
security(("Bearer" = ["settings.update", "settings.*", "*"])),
|
||||
summary = concat!("Reset ", $camelcase_attr),
|
||||
description = concat!("Reset an index's ", $camelcase_attr, " to its default value"),
|
||||
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
request_body = $type,
|
||||
responses(
|
||||
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"taskUid": 147,
|
||||
"indexUid": "movies",
|
||||
"status": "enqueued",
|
||||
"type": "settingsUpdate",
|
||||
"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(
|
||||
index_scheduler: GuardedData<
|
||||
ActionPolicy<{ actions::SETTINGS_UPDATE }>,
|
||||
@ -96,6 +143,36 @@ macro_rules! make_setting_route {
|
||||
Ok(HttpResponse::Accepted().json(task))
|
||||
}
|
||||
|
||||
|
||||
#[utoipa::path(
|
||||
$update_verb,
|
||||
path = concat!("{indexUid}/settings", $route),
|
||||
tag = "Settings",
|
||||
security(("Bearer" = ["settings.update", "settings.*", "*"])),
|
||||
summary = concat!("Update ", $camelcase_attr),
|
||||
description = concat!("Update an index's user defined ", $camelcase_attr),
|
||||
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
request_body = $type,
|
||||
responses(
|
||||
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"taskUid": 147,
|
||||
"indexUid": "movies",
|
||||
"status": "enqueued",
|
||||
"type": "settingsUpdate",
|
||||
"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(
|
||||
index_scheduler: GuardedData<
|
||||
ActionPolicy<{ actions::SETTINGS_UPDATE }>,
|
||||
@ -151,6 +228,29 @@ macro_rules! make_setting_route {
|
||||
Ok(HttpResponse::Accepted().json(task))
|
||||
}
|
||||
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = concat!("{indexUid}/settings", $route),
|
||||
tag = "Settings",
|
||||
summary = concat!("Get ", $camelcase_attr),
|
||||
description = concat!("Get an user defined ", $camelcase_attr),
|
||||
security(("Bearer" = ["settings.get", "settings.*", "*"])),
|
||||
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
responses(
|
||||
(status = 200, description = concat!($camelcase_attr, " is returned"), body = $type, content_type = "application/json", example = json!(
|
||||
<$type>::default()
|
||||
)),
|
||||
(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_scheduler: GuardedData<
|
||||
ActionPolicy<{ actions::SETTINGS_GET }>,
|
||||
@ -359,7 +459,7 @@ make_setting_routes!(
|
||||
{
|
||||
route: "/embedders",
|
||||
update_verb: patch,
|
||||
value_type: std::collections::BTreeMap<String, Setting<meilisearch_types::milli::vector::settings::EmbeddingSettings>>,
|
||||
value_type: std::collections::BTreeMap<String, SettingEmbeddingSettings>,
|
||||
err_type: meilisearch_types::deserr::DeserrJsonError<
|
||||
meilisearch_types::error::deserr_codes::InvalidSettingsEmbedders,
|
||||
>,
|
||||
@ -402,6 +502,39 @@ make_setting_routes!(
|
||||
},
|
||||
);
|
||||
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "{indexUid}/settings",
|
||||
tag = "Settings",
|
||||
security(("Bearer" = ["settings.update", "settings.*", "*"])),
|
||||
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
request_body = Settings<Unchecked>,
|
||||
responses(
|
||||
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"taskUid": 147,
|
||||
"indexUid": "movies",
|
||||
"status": "enqueued",
|
||||
"type": "settingsUpdate",
|
||||
"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"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
/// Update settings
|
||||
///
|
||||
/// Update the settings of an index.
|
||||
/// Passing null to an index setting will reset it to its default value.
|
||||
/// Updates in the settings route are partial. This means that any parameters not provided in the body will be left unchanged.
|
||||
/// If the provided index does not exist, it will be created.
|
||||
pub async fn update_all(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_UPDATE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -479,6 +612,29 @@ pub async fn update_all(
|
||||
Ok(HttpResponse::Accepted().json(task))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "{indexUid}/settings",
|
||||
tag = "Settings",
|
||||
security(("Bearer" = ["settings.update", "settings.*", "*"])),
|
||||
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
responses(
|
||||
(status = 200, description = "Settings are returned", body = Settings<Unchecked>, content_type = "application/json", example = json!(
|
||||
Settings::<Unchecked>::default()
|
||||
)),
|
||||
(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"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
/// All settings
|
||||
///
|
||||
/// This route allows you to retrieve, configure, or reset all of an index's settings at once.
|
||||
pub async fn get_all(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_GET }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -492,6 +648,35 @@ pub async fn get_all(
|
||||
Ok(HttpResponse::Ok().json(new_settings))
|
||||
}
|
||||
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "{indexUid}/settings",
|
||||
tag = "Settings",
|
||||
security(("Bearer" = ["settings.update", "settings.*", "*"])),
|
||||
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
responses(
|
||||
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"taskUid": 147,
|
||||
"indexUid": "movies",
|
||||
"status": "enqueued",
|
||||
"type": "settingsUpdate",
|
||||
"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"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
/// Reset settings
|
||||
///
|
||||
/// Reset all the settings of an index to their default value.
|
||||
pub async fn delete_all(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SETTINGS_UPDATE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
|
@ -8,10 +8,9 @@ use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use meilisearch_types::facet_values_sort::FacetValuesSort;
|
||||
use meilisearch_types::locales::{Locale, LocalizedAttributesRuleView};
|
||||
use meilisearch_types::milli::update::Setting;
|
||||
use meilisearch_types::milli::vector::settings::EmbeddingSettings;
|
||||
use meilisearch_types::settings::{
|
||||
FacetingSettings, PaginationSettings, PrefixSearchSettings, ProximityPrecisionView,
|
||||
RankingRuleView, TypoSettings,
|
||||
RankingRuleView, SettingEmbeddingSettings, TypoSettings,
|
||||
};
|
||||
use serde::Serialize;
|
||||
|
||||
@ -497,13 +496,13 @@ pub struct EmbeddersAnalytics {
|
||||
}
|
||||
|
||||
impl EmbeddersAnalytics {
|
||||
pub fn new(setting: Option<&BTreeMap<String, Setting<EmbeddingSettings>>>) -> Self {
|
||||
pub fn new(setting: Option<&BTreeMap<String, SettingEmbeddingSettings>>) -> Self {
|
||||
let mut sources = std::collections::HashSet::new();
|
||||
|
||||
if let Some(s) = &setting {
|
||||
for source in s
|
||||
.values()
|
||||
.filter_map(|config| config.clone().set())
|
||||
.filter_map(|config| config.inner.clone().set())
|
||||
.filter_map(|config| config.source.set())
|
||||
{
|
||||
use meilisearch_types::milli::vector::settings::EmbedderSource;
|
||||
@ -522,18 +521,18 @@ impl EmbeddersAnalytics {
|
||||
sources: Some(sources),
|
||||
document_template_used: setting.as_ref().map(|map| {
|
||||
map.values()
|
||||
.filter_map(|config| config.clone().set())
|
||||
.filter_map(|config| config.inner.clone().set())
|
||||
.any(|config| config.document_template.set().is_some())
|
||||
}),
|
||||
document_template_max_bytes: setting.as_ref().and_then(|map| {
|
||||
map.values()
|
||||
.filter_map(|config| config.clone().set())
|
||||
.filter_map(|config| config.inner.clone().set())
|
||||
.filter_map(|config| config.document_template_max_bytes.set())
|
||||
.max()
|
||||
}),
|
||||
binary_quantization_used: setting.as_ref().map(|map| {
|
||||
map.values()
|
||||
.filter_map(|config| config.clone().set())
|
||||
.filter_map(|config| config.inner.clone().set())
|
||||
.any(|config| config.binary_quantized.set().is_some())
|
||||
}),
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ use meilisearch_types::keys::actions;
|
||||
use meilisearch_types::serde_cs::vec::CS;
|
||||
use serde_json::Value;
|
||||
use tracing::debug;
|
||||
use utoipa::{IntoParams, OpenApi};
|
||||
|
||||
use super::ActionPolicy;
|
||||
use crate::analytics::Analytics;
|
||||
@ -22,6 +23,21 @@ use crate::search::{
|
||||
SimilarQuery, SimilarResult, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET,
|
||||
};
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(similar_get, similar_post),
|
||||
tags(
|
||||
(
|
||||
name = "Similar documents",
|
||||
description = "The /similar route uses AI-powered search to return a number of documents similar to a target document.
|
||||
|
||||
Meilisearch exposes two routes for retrieving similar documents: POST and GET. In the majority of cases, POST will offer better performance and ease of use.",
|
||||
external_docs(url = "https://www.meilisearch.com/docs/reference/api/similar"),
|
||||
),
|
||||
),
|
||||
)]
|
||||
pub struct SimilarApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
@ -30,6 +46,62 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Get similar documents with GET
|
||||
///
|
||||
/// Retrieve documents similar to a specific search result.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "{indexUid}/similar",
|
||||
tag = "Similar documents",
|
||||
security(("Bearer" = ["search", "*"])),
|
||||
params(
|
||||
("indexUid" = String, Path, example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
SimilarQueryGet
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "The documents are returned", body = SimilarResult, content_type = "application/json", example = json!(
|
||||
{
|
||||
"hits": [
|
||||
{
|
||||
"id": 2770,
|
||||
"title": "American Pie 2",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/q4LNgUnRfltxzp3gf1MAGiK5LhV.jpg",
|
||||
"overview": "The whole gang are back and as close as ever. They decide to get even closer by spending the summer together at a beach house. They decide to hold the biggest…",
|
||||
"release_date": 997405200
|
||||
},
|
||||
{
|
||||
"id": 190859,
|
||||
"title": "American Sniper",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/svPHnYE7N5NAGO49dBmRhq0vDQ3.jpg",
|
||||
"overview": "U.S. Navy SEAL Chris Kyle takes his sole mission—protect his comrades—to heart and becomes one of the most lethal snipers in American history. His pinpoint accuracy not only saves countless lives but also makes him a prime…",
|
||||
"release_date": 1418256000
|
||||
}
|
||||
],
|
||||
"offset": 0,
|
||||
"limit": 2,
|
||||
"estimatedTotalHits": 976,
|
||||
"processingTimeMs": 35,
|
||||
"query": "american "
|
||||
}
|
||||
)),
|
||||
(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 similar_get(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -58,6 +130,60 @@ pub async fn similar_get(
|
||||
Ok(HttpResponse::Ok().json(similar))
|
||||
}
|
||||
|
||||
/// Get similar documents with POST
|
||||
///
|
||||
/// Retrieve documents similar to a specific search result.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "{indexUid}/similar",
|
||||
tag = "Similar documents",
|
||||
security(("Bearer" = ["search", "*"])),
|
||||
params(("indexUid" = String, Path, example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
request_body = SimilarQuery,
|
||||
responses(
|
||||
(status = 200, description = "The documents are returned", body = SimilarResult, content_type = "application/json", example = json!(
|
||||
{
|
||||
"hits": [
|
||||
{
|
||||
"id": 2770,
|
||||
"title": "American Pie 2",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/q4LNgUnRfltxzp3gf1MAGiK5LhV.jpg",
|
||||
"overview": "The whole gang are back and as close as ever. They decide to get even closer by spending the summer together at a beach house. They decide to hold the biggest…",
|
||||
"release_date": 997405200
|
||||
},
|
||||
{
|
||||
"id": 190859,
|
||||
"title": "American Sniper",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/svPHnYE7N5NAGO49dBmRhq0vDQ3.jpg",
|
||||
"overview": "U.S. Navy SEAL Chris Kyle takes his sole mission—protect his comrades—to heart and becomes one of the most lethal snipers in American history. His pinpoint accuracy not only saves countless lives but also makes him a prime…",
|
||||
"release_date": 1418256000
|
||||
}
|
||||
],
|
||||
"offset": 0,
|
||||
"limit": 2,
|
||||
"estimatedTotalHits": 976,
|
||||
"processingTimeMs": 35,
|
||||
"query": "american "
|
||||
}
|
||||
)),
|
||||
(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 similar_post(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
@ -125,26 +251,35 @@ async fn similar(
|
||||
.await?
|
||||
}
|
||||
|
||||
#[derive(Debug, deserr::Deserr)]
|
||||
#[derive(Debug, deserr::Deserr, IntoParams)]
|
||||
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[into_params(parameter_in = Query)]
|
||||
pub struct SimilarQueryGet {
|
||||
#[deserr(error = DeserrQueryParamError<InvalidSimilarId>)]
|
||||
#[param(value_type = String)]
|
||||
id: Param<String>,
|
||||
#[deserr(default = Param(DEFAULT_SEARCH_OFFSET()), error = DeserrQueryParamError<InvalidSimilarOffset>)]
|
||||
#[param(value_type = usize, default = DEFAULT_SEARCH_OFFSET)]
|
||||
offset: Param<usize>,
|
||||
#[deserr(default = Param(DEFAULT_SEARCH_LIMIT()), error = DeserrQueryParamError<InvalidSimilarLimit>)]
|
||||
#[param(value_type = usize, default = DEFAULT_SEARCH_LIMIT)]
|
||||
limit: Param<usize>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarAttributesToRetrieve>)]
|
||||
#[param(value_type = Vec<String>)]
|
||||
attributes_to_retrieve: Option<CS<String>>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarRetrieveVectors>)]
|
||||
#[param(value_type = bool, default)]
|
||||
retrieve_vectors: Param<bool>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarFilter>)]
|
||||
filter: Option<String>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarShowRankingScore>)]
|
||||
#[param(value_type = bool, default)]
|
||||
show_ranking_score: Param<bool>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarShowRankingScoreDetails>)]
|
||||
#[param(value_type = bool, default)]
|
||||
show_ranking_score_details: Param<bool>,
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidSimilarRankingScoreThreshold>, default)]
|
||||
#[param(value_type = Option<f32>)]
|
||||
pub ranking_score_threshold: Option<RankingScoreThresholdGet>,
|
||||
#[deserr(error = DeserrQueryParamError<InvalidEmbedder>)]
|
||||
pub embedder: String,
|
||||
|
@ -14,9 +14,11 @@ use index_scheduler::IndexScheduler;
|
||||
use meilisearch_types::deserr::DeserrJsonError;
|
||||
use meilisearch_types::error::deserr_codes::*;
|
||||
use meilisearch_types::error::{Code, ResponseError};
|
||||
use serde::Serialize;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing_subscriber::filter::Targets;
|
||||
use tracing_subscriber::Layer;
|
||||
use utoipa::{OpenApi, ToSchema};
|
||||
|
||||
use crate::error::MeilisearchHttpError;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
@ -24,6 +26,18 @@ use crate::extractors::authentication::GuardedData;
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
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) {
|
||||
cfg.service(
|
||||
web::resource("stream")
|
||||
@ -33,12 +47,16 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.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)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub enum LogMode {
|
||||
/// Output the logs in a human readable form.
|
||||
#[default]
|
||||
Human,
|
||||
/// Output the logs in json.
|
||||
Json,
|
||||
/// Output the logs in the firefox profiler format. They can then be loaded and visualized at https://profiler.firefox.com/
|
||||
Profile,
|
||||
}
|
||||
|
||||
@ -83,16 +101,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>)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
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>)]
|
||||
#[schema(value_type = String, default = "info", example = json!("milli=trace,index_scheduler,actix_web=off"))]
|
||||
target: MyTargets,
|
||||
|
||||
/// Lets you customize the format of the logs.
|
||||
#[deserr(default, error = DeserrJsonError<BadRequest>)]
|
||||
#[schema(default = LogMode::default)]
|
||||
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>)]
|
||||
#[schema(default = false)]
|
||||
profile_memory: bool,
|
||||
}
|
||||
|
||||
@ -248,6 +276,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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>,
|
||||
logs: Data<LogRouteHandle>,
|
||||
@ -280,6 +348,26 @@ 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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>,
|
||||
logs: Data<LogRouteHandle>,
|
||||
@ -293,13 +381,38 @@ pub async fn cancel_logs(
|
||||
Ok(HttpResponse::NoContent().finish())
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserr)]
|
||||
#[derive(Debug, Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
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>)]
|
||||
#[schema(value_type = String, default = "info", example = json!("milli=trace,index_scheduler,actix_web=off"))]
|
||||
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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>,
|
||||
logs: Data<LogStderrHandle>,
|
||||
|
@ -1,3 +1,7 @@
|
||||
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::web::{self, Data};
|
||||
use actix_web::HttpResponse;
|
||||
@ -8,16 +12,102 @@ use meilisearch_types::keys::actions;
|
||||
use meilisearch_types::tasks::Status;
|
||||
use prometheus::{Encoder, TextEncoder};
|
||||
use time::OffsetDateTime;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use crate::extractors::authentication::policies::ActionPolicy;
|
||||
use crate::extractors::authentication::{AuthenticationError, GuardedData};
|
||||
use crate::routes::create_all_stats;
|
||||
use crate::search_queue::SearchQueue;
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(paths(get_metrics))]
|
||||
pub struct MetricApi;
|
||||
|
||||
pub fn configure(config: &mut web::ServiceConfig) {
|
||||
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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>,
|
||||
auth_controller: Data<AuthController>,
|
||||
|
@ -4,19 +4,46 @@ 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::batch_view::BatchView;
|
||||
use meilisearch_types::batches::BatchStats;
|
||||
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 self::api_key::KeyView;
|
||||
use self::indexes::documents::BrowseQuery;
|
||||
use self::indexes::{IndexCreateRequest, IndexStats, UpdateIndexRequest};
|
||||
use self::logs::{GetLogs, LogMode, UpdateStderrLogs};
|
||||
use self::open_api_utils::OpenApiAuth;
|
||||
use self::tasks::AllTasks;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::GuardedData;
|
||||
use crate::milli::progress::{ProgressStepView, ProgressView};
|
||||
use crate::routes::batches::AllBatches;
|
||||
use crate::routes::features::RuntimeTogglableFeatures;
|
||||
use crate::routes::indexes::documents::{DocumentDeletionByFilter, DocumentEditionByFunction};
|
||||
use crate::routes::indexes::IndexView;
|
||||
use crate::routes::multi_search::SearchResults;
|
||||
use crate::routes::swap_indexes::SwapIndexesPayload;
|
||||
use crate::search::{
|
||||
FederatedSearch, FederatedSearchResult, Federation, FederationOptions, MergeFacets,
|
||||
SearchQueryWithIndex, SearchResultWithIndex, SimilarQuery, SimilarResult,
|
||||
};
|
||||
use crate::search_queue::SearchQueue;
|
||||
use crate::Opt;
|
||||
|
||||
const PAGINATION_DEFAULT_LIMIT: usize = 20;
|
||||
const PAGINATION_DEFAULT_LIMIT_FN: fn() -> usize = || 20;
|
||||
|
||||
mod api_key;
|
||||
pub mod batches;
|
||||
@ -27,10 +54,37 @@ mod logs;
|
||||
mod metrics;
|
||||
mod multi_search;
|
||||
mod multi_search_analytics;
|
||||
mod open_api_utils;
|
||||
mod snapshot;
|
||||
mod swap_indexes;
|
||||
pub mod tasks;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
nest(
|
||||
(path = "/tasks", api = tasks::TaskApi),
|
||||
(path = "/batches", api = batches::BatchesApi),
|
||||
(path = "/indexes", api = indexes::IndexesApi),
|
||||
// We must stop the search path here because the rest must be configured by each route individually
|
||||
(path = "/indexes", api = indexes::search::SearchApi),
|
||||
(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),
|
||||
(path = "/multi-search", api = multi_search::MultiSearchApi),
|
||||
(path = "/swap-indexes", api = swap_indexes::SwapIndexesApi),
|
||||
(path = "/experimental-features", api = features::ExperimentalFeaturesApi),
|
||||
),
|
||||
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(PaginationView<KeyView>, PaginationView<IndexView>, IndexView, DocumentDeletionByFilter, AllBatches, BatchStats, ProgressStepView, ProgressView, BatchView, RuntimeTogglableFeatures, SwapIndexesPayload, DocumentEditionByFunction, MergeFacets, FederationOptions, SearchQueryWithIndex, Federation, FederatedSearch, FederatedSearchResult, SearchResults, SearchResultWithIndex, SimilarQuery, SimilarResult, PaginationView<serde_json::Value>, 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) {
|
||||
cfg.service(web::scope("/tasks").configure(tasks::configure))
|
||||
.service(web::scope("/batches").configure(batches::configure))
|
||||
@ -46,6 +100,13 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.service(web::scope("/swap-indexes").configure(swap_indexes::configure))
|
||||
.service(web::scope("/metrics").configure(metrics::configure))
|
||||
.service(web::scope("/experimental-features").configure(features::configure));
|
||||
|
||||
#[cfg(feature = "swagger")]
|
||||
{
|
||||
use utoipa_scalar::{Scalar, Servable as ScalarServable};
|
||||
let openapi = MeilisearchApi::openapi();
|
||||
cfg.service(Scalar::with_url("/scalar", openapi.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_task_id(req: &HttpRequest, opt: &Opt) -> Result<Option<TaskId>, ResponseError> {
|
||||
@ -98,14 +159,20 @@ pub fn is_dry_run(req: &HttpRequest, opt: &Opt) -> Result<bool, ResponseError> {
|
||||
.map_or(false, |s| s.to_lowercase() == "true"))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SummarizedTaskView {
|
||||
/// The task unique identifier.
|
||||
#[schema(value_type = u32)]
|
||||
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>,
|
||||
/// The status of the task.
|
||||
status: Status,
|
||||
/// The type of the task.
|
||||
#[serde(rename = "type")]
|
||||
kind: Kind,
|
||||
/// The date on which the task was enqueued.
|
||||
#[serde(serialize_with = "time::serde::rfc3339::serialize")]
|
||||
enqueued_at: OffsetDateTime,
|
||||
}
|
||||
@ -127,7 +194,9 @@ pub struct Pagination {
|
||||
pub limit: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[derive(Debug, Clone, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct PaginationView<T> {
|
||||
pub results: Vec<T>,
|
||||
pub offset: usize,
|
||||
@ -283,17 +352,56 @@ pub async fn running() -> HttpResponse {
|
||||
HttpResponse::Ok().json(serde_json::json!({ "status": "Meilisearch is running" }))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize, Debug, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Stats {
|
||||
/// The size of the database, in bytes.
|
||||
pub database_size: u64,
|
||||
#[serde(skip)]
|
||||
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")]
|
||||
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>,
|
||||
}
|
||||
|
||||
/// 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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<IndexScheduler>>,
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<AuthController>>,
|
||||
@ -343,14 +451,43 @@ pub fn create_all_stats(
|
||||
Ok(stats)
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
#[derive(Serialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct VersionResponse {
|
||||
/// The commit used to compile this build of Meilisearch.
|
||||
commit_sha: String,
|
||||
/// The date of this build.
|
||||
commit_date: String,
|
||||
/// The version of Meilisearch.
|
||||
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(
|
||||
_index_scheduler: GuardedData<ActionPolicy<{ actions::VERSION }>, Data<IndexScheduler>>,
|
||||
) -> HttpResponse {
|
||||
@ -370,6 +507,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(
|
||||
index_scheduler: Data<IndexScheduler>,
|
||||
auth_controller: Data<AuthController>,
|
||||
@ -379,5 +545,5 @@ pub async fn get_health(
|
||||
index_scheduler.health().unwrap();
|
||||
auth_controller.health().unwrap();
|
||||
|
||||
Ok(HttpResponse::Ok().json(serde_json::json!({ "status": "available" })))
|
||||
Ok(HttpResponse::Ok().json(&HealthResponse::default()))
|
||||
}
|
||||
|
@ -8,6 +8,7 @@ use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::keys::actions;
|
||||
use serde::Serialize;
|
||||
use tracing::debug;
|
||||
use utoipa::{OpenApi, ToSchema};
|
||||
|
||||
use super::multi_search_analytics::MultiSearchAggregator;
|
||||
use crate::analytics::Analytics;
|
||||
@ -17,20 +18,127 @@ use crate::extractors::authentication::{AuthenticationError, GuardedData};
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
use crate::routes::indexes::search::search_kind;
|
||||
use crate::search::{
|
||||
add_search_rules, perform_federated_search, perform_search, FederatedSearch, RetrieveVectors,
|
||||
SearchQueryWithIndex, SearchResultWithIndex,
|
||||
add_search_rules, perform_federated_search, perform_search, FederatedSearch,
|
||||
FederatedSearchResult, RetrieveVectors, SearchQueryWithIndex, SearchResultWithIndex,
|
||||
};
|
||||
use crate::search_queue::SearchQueue;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
paths(multi_search_with_post),
|
||||
tags((
|
||||
name = "Multi-search",
|
||||
description = "The `/multi-search` route allows you to perform multiple search queries on one or more indexes by bundling them into a single HTTP request. Multi-search is also known as federated search.",
|
||||
external_docs(url = "https://www.meilisearch.com/docs/reference/api/multi_search"),
|
||||
)),
|
||||
)]
|
||||
pub struct MultiSearchApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::resource("").route(web::post().to(SeqHandler(multi_search_with_post))));
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct SearchResults {
|
||||
#[derive(Serialize, ToSchema)]
|
||||
pub struct SearchResults {
|
||||
results: Vec<SearchResultWithIndex>,
|
||||
}
|
||||
|
||||
/// Perform a multi-search
|
||||
///
|
||||
/// Bundle multiple search queries in a single API request. Use this endpoint to search through multiple indexes at once.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/",
|
||||
tag = "Multi-search",
|
||||
security(("Bearer" = ["search", "*"])),
|
||||
responses(
|
||||
(status = OK, description = "Non federated multi-search", body = SearchResults, content_type = "application/json", example = json!(
|
||||
{
|
||||
"results":[
|
||||
{
|
||||
"indexUid":"movies",
|
||||
"hits":[
|
||||
{
|
||||
"id":13682,
|
||||
"title":"Pooh's Heffalump Movie",
|
||||
},
|
||||
],
|
||||
"query":"pooh",
|
||||
"processingTimeMs":26,
|
||||
"limit":1,
|
||||
"offset":0,
|
||||
"estimatedTotalHits":22
|
||||
},
|
||||
{
|
||||
"indexUid":"movies",
|
||||
"hits":[
|
||||
{
|
||||
"id":12,
|
||||
"title":"Finding Nemo",
|
||||
},
|
||||
],
|
||||
"query":"nemo",
|
||||
"processingTimeMs":5,
|
||||
"limit":1,
|
||||
"offset":0,
|
||||
"estimatedTotalHits":11
|
||||
},
|
||||
{
|
||||
"indexUid":"movie_ratings",
|
||||
"hits":[
|
||||
{
|
||||
"id":"Us",
|
||||
"director": "Jordan Peele",
|
||||
}
|
||||
],
|
||||
"query":"Us",
|
||||
"processingTimeMs":0,
|
||||
"limit":1,
|
||||
"offset":0,
|
||||
"estimatedTotalHits":1
|
||||
}
|
||||
]
|
||||
}
|
||||
)),
|
||||
(status = OK, description = "Federated multi-search", body = FederatedSearchResult, content_type = "application/json", example = json!(
|
||||
{
|
||||
"hits": [
|
||||
{
|
||||
"id": 42,
|
||||
"title": "Batman returns",
|
||||
"overview": "The overview of batman returns",
|
||||
"_federation": {
|
||||
"indexUid": "movies",
|
||||
"queriesPosition": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"comicsId": "batman-killing-joke",
|
||||
"description": "This comic is really awesome",
|
||||
"title": "Batman: the killing joke",
|
||||
"_federation": {
|
||||
"indexUid": "comics",
|
||||
"queriesPosition": 1
|
||||
}
|
||||
},
|
||||
],
|
||||
"processingTimeMs": 0,
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"estimatedTotalHits": 2,
|
||||
"semanticHitCount": 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"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn multi_search_with_post(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SEARCH }>, Data<IndexScheduler>>,
|
||||
search_queue: Data<SearchQueue>,
|
||||
|
24
crates/meilisearch/src/routes/open_api_utils.rs
Normal file
24
crates/meilisearch/src/routes/open_api_utils.rs
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ use index_scheduler::IndexScheduler;
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::tasks::KindWithContent;
|
||||
use tracing::debug;
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use crate::analytics::Analytics;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
@ -12,12 +13,55 @@ use crate::extractors::sequential_extractor::SeqHandler;
|
||||
use crate::routes::{get_task_id, is_dry_run, SummarizedTaskView};
|
||||
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 exported—together with their documents and settings—and 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) {
|
||||
cfg.service(web::resource("").route(web::post().to(SeqHandler(create_snapshot))));
|
||||
}
|
||||
|
||||
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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::SNAPSHOTS_CREATE }>, Data<IndexScheduler>>,
|
||||
req: HttpRequest,
|
||||
|
@ -9,6 +9,7 @@ use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::index_uid::IndexUid;
|
||||
use meilisearch_types::tasks::{IndexSwap, KindWithContent};
|
||||
use serde::Serialize;
|
||||
use utoipa::{OpenApi, ToSchema};
|
||||
|
||||
use super::{get_task_id, is_dry_run, SummarizedTaskView};
|
||||
use crate::analytics::{Aggregate, Analytics};
|
||||
@ -18,13 +19,18 @@ use crate::extractors::authentication::{AuthenticationError, GuardedData};
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
use crate::Opt;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(paths(swap_indexes))]
|
||||
pub struct SwapIndexesApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::resource("").route(web::post().to(SeqHandler(swap_indexes))));
|
||||
}
|
||||
|
||||
#[derive(Deserr, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Deserr, Debug, Clone, PartialEq, Eq, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct SwapIndexesPayload {
|
||||
/// Array of the two indexUids to be swapped
|
||||
#[deserr(error = DeserrJsonError<InvalidSwapIndexes>, missing_field_error = DeserrJsonError::missing_swap_indexes)]
|
||||
indexes: Vec<IndexUid>,
|
||||
}
|
||||
@ -50,6 +56,37 @@ impl Aggregate for IndexSwappedAnalytics {
|
||||
}
|
||||
}
|
||||
|
||||
/// Swap indexes
|
||||
///
|
||||
/// Swap the documents, settings, and task history of two or more indexes. You can only swap indexes in pairs. However, a single request can swap as many index pairs as you wish.
|
||||
/// Swapping indexes is an atomic transaction: either all indexes are successfully swapped, or none are.
|
||||
/// Swapping indexA and indexB will also replace every mention of indexA by indexB and vice-versa in the task history. enqueued tasks are left unmodified.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/",
|
||||
tag = "Indexes",
|
||||
security(("Bearer" = ["search", "*"])),
|
||||
request_body = Vec<SwapIndexesPayload>,
|
||||
responses(
|
||||
(status = OK, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"taskUid": 3,
|
||||
"indexUid": null,
|
||||
"status": "enqueued",
|
||||
"type": "indexSwap",
|
||||
"enqueuedAt": "2021-08-12T10:00: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 swap_indexes(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_SWAP }>, Data<IndexScheduler>>,
|
||||
params: AwebJson<Vec<SwapIndexesPayload>, DeserrJsonError>,
|
||||
|
@ -17,6 +17,7 @@ use time::format_description::well_known::Rfc3339;
|
||||
use time::macros::format_description;
|
||||
use time::{Date, Duration, OffsetDateTime, Time};
|
||||
use tokio::task;
|
||||
use utoipa::{IntoParams, OpenApi, ToSchema};
|
||||
|
||||
use super::{get_task_id, is_dry_run, SummarizedTaskView, PAGINATION_DEFAULT_LIMIT};
|
||||
use crate::analytics::{Aggregate, AggregateMethod, Analytics};
|
||||
@ -25,6 +26,17 @@ use crate::extractors::authentication::GuardedData;
|
||||
use crate::extractors::sequential_extractor::SeqHandler;
|
||||
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) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
@ -35,41 +47,72 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
.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)]
|
||||
#[into_params(rename_all = "camelCase", parameter_in = Query)]
|
||||
pub struct TasksFilterQuery {
|
||||
/// Maximum number of results to return.
|
||||
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT as u32), error = DeserrQueryParamError<InvalidTaskLimit>)]
|
||||
#[param(required = false, value_type = u32, example = 12, default = json!(PAGINATION_DEFAULT_LIMIT))]
|
||||
pub limit: Param<u32>,
|
||||
/// Fetch the next set of results from the given uid.
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidTaskFrom>)]
|
||||
#[param(required = false, value_type = Option<u32>, example = 12421)]
|
||||
pub from: Option<Param<TaskId>>,
|
||||
/// The order you want to retrieve the objects.
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidTaskReverse>)]
|
||||
#[param(required = false, value_type = Option<bool>, example = true)]
|
||||
pub reverse: Option<Param<bool>>,
|
||||
|
||||
/// Permits to filter tasks by their batch uid. By default, when the `batchUids` query parameter is not set, all task uids are returned. It's possible to specify several batch uids by separating them with the `,` character.
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidBatchUids>)]
|
||||
#[param(required = false, value_type = Option<u32>, example = 12421)]
|
||||
pub batch_uids: OptionStarOrList<BatchId>,
|
||||
|
||||
/// 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>)]
|
||||
#[param(required = false, value_type = Option<Vec<u32>>, example = json!([231, 423, 598, "*"]))]
|
||||
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>)]
|
||||
#[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>)]
|
||||
#[param(required = false, value_type = Option<Vec<String>>, example = json!([Kind::DocumentAdditionOrUpdate, "*"]))]
|
||||
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>)]
|
||||
#[param(required = false, value_type = Option<Vec<Status>>, example = json!([Status::Succeeded, Status::Failed, Status::Canceled, Status::Enqueued, Status::Processing, "*"]))]
|
||||
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>)]
|
||||
#[param(required = false, value_type = Option<Vec<String>>, example = json!(["movies", "theater", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
pub before_finished_at: OptionStarOr<OffsetDateTime>,
|
||||
}
|
||||
|
||||
@ -117,33 +160,58 @@ impl TaskDeletionOrCancelationQuery {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserr)]
|
||||
#[derive(Debug, Deserr, IntoParams)]
|
||||
#[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[into_params(rename_all = "camelCase", parameter_in = Query)]
|
||||
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>)]
|
||||
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>)]
|
||||
#[param(required = false, value_type = Option<Vec<u32>>, example = json!([231, 423, 598, "*"]))]
|
||||
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>)]
|
||||
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>)]
|
||||
#[param(required = false, value_type = Option<Vec<Kind>>, example = json!([Kind::DocumentDeletion, "*"]))]
|
||||
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>)]
|
||||
#[param(required = false, value_type = Option<Vec<Status>>, example = json!([Status::Succeeded, Status::Failed, Status::Canceled, "*"]))]
|
||||
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>)]
|
||||
#[param(required = false, value_type = Option<Vec<String>>, example = json!(["movies", "theater", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
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)]
|
||||
#[param(required = false, value_type = Option<String>, example = json!(["2024-08-08T16:37:09.971Z", "*"]))]
|
||||
pub before_finished_at: OptionStarOr<OffsetDateTime>,
|
||||
}
|
||||
|
||||
@ -226,6 +294,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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_CANCEL }>, Data<IndexScheduler>>,
|
||||
params: AwebQueryParameter<TaskDeletionOrCancelationQuery, DeserrQueryParamError>,
|
||||
@ -275,6 +388,51 @@ async fn cancel_tasks(
|
||||
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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_DELETE }>, Data<IndexScheduler>>,
|
||||
params: AwebQueryParameter<TaskDeletionOrCancelationQuery, DeserrQueryParamError>,
|
||||
@ -323,15 +481,69 @@ async fn delete_tasks(
|
||||
Ok(HttpResponse::Ok().json(task))
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[derive(Debug, Serialize, ToSchema)]
|
||||
pub struct AllTasks {
|
||||
/// The list of tasks that matched the filter.
|
||||
results: Vec<TaskView>,
|
||||
/// Total number of browsable results using offset/limit parameters for the given resource.
|
||||
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,
|
||||
/// The first task uid returned.
|
||||
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>,
|
||||
}
|
||||
|
||||
/// 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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_GET }>, Data<IndexScheduler>>,
|
||||
params: AwebQueryParameter<TasksFilterQuery, DeserrQueryParamError>,
|
||||
@ -356,6 +568,52 @@ async fn get_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(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_GET }>, Data<IndexScheduler>>,
|
||||
task_uid: web::Path<String>,
|
||||
|
@ -22,6 +22,7 @@ use meilisearch_types::milli::score_details::{ScoreDetails, ScoreValue};
|
||||
use meilisearch_types::milli::{self, DocumentId, OrderBy, TimeBudget};
|
||||
use roaring::RoaringBitmap;
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use super::ranking_rules::{self, RankingRules};
|
||||
use super::{
|
||||
@ -33,10 +34,11 @@ use crate::routes::indexes::search::search_kind;
|
||||
|
||||
pub const DEFAULT_FEDERATED_WEIGHT: f64 = 1.0;
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, deserr::Deserr)]
|
||||
#[derive(Debug, Default, Clone, Copy, PartialEq, deserr::Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct FederationOptions {
|
||||
#[deserr(default, error = DeserrJsonError<InvalidMultiSearchWeight>)]
|
||||
#[schema(value_type = f64)]
|
||||
pub weight: Weight,
|
||||
}
|
||||
|
||||
@ -70,8 +72,9 @@ impl std::ops::Deref for Weight {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, deserr::Deserr)]
|
||||
#[derive(Debug, deserr::Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct Federation {
|
||||
#[deserr(default = super::DEFAULT_SEARCH_LIMIT(), error = DeserrJsonError<InvalidSearchLimit>)]
|
||||
pub limit: usize,
|
||||
@ -83,22 +86,26 @@ pub struct Federation {
|
||||
pub merge_facets: Option<MergeFacets>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, deserr::Deserr, Default)]
|
||||
#[derive(Copy, Clone, Debug, deserr::Deserr, Default, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError<InvalidMultiSearchMergeFacets>, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct MergeFacets {
|
||||
#[deserr(default, error = DeserrJsonError<InvalidMultiSearchMaxValuesPerFacet>)]
|
||||
pub max_values_per_facet: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Debug, deserr::Deserr)]
|
||||
#[derive(Debug, deserr::Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct FederatedSearch {
|
||||
pub queries: Vec<SearchQueryWithIndex>,
|
||||
#[deserr(default)]
|
||||
pub federation: Option<Federation>,
|
||||
}
|
||||
#[derive(Serialize, Clone)]
|
||||
|
||||
#[derive(Serialize, Clone, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct FederatedSearchResult {
|
||||
pub hits: Vec<SearchHit>,
|
||||
pub processing_time_ms: u128,
|
||||
@ -109,6 +116,7 @@ pub struct FederatedSearchResult {
|
||||
pub semantic_hit_count: Option<u32>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(value_type = Option<BTreeMap<String, BTreeMap<String, u64>>>)]
|
||||
pub facet_distribution: Option<BTreeMap<String, IndexMap<String, u64>>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub facet_stats: Option<BTreeMap<String, FacetStats>>,
|
||||
@ -355,7 +363,7 @@ struct SearchResultByIndex {
|
||||
facets: Option<ComputedFacets>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, ToSchema)]
|
||||
pub struct FederatedFacets(pub BTreeMap<String, ComputedFacets>);
|
||||
|
||||
impl FederatedFacets {
|
||||
|
@ -34,11 +34,15 @@ use serde::Serialize;
|
||||
use serde_json::{json, Value};
|
||||
#[cfg(test)]
|
||||
mod mod_test;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::error::MeilisearchHttpError;
|
||||
|
||||
mod federated;
|
||||
pub use federated::{perform_federated_search, FederatedSearch, Federation, FederationOptions};
|
||||
pub use federated::{
|
||||
perform_federated_search, FederatedSearch, FederatedSearchResult, Federation,
|
||||
FederationOptions, MergeFacets,
|
||||
};
|
||||
|
||||
mod ranking_rules;
|
||||
|
||||
@ -52,7 +56,7 @@ pub const DEFAULT_HIGHLIGHT_PRE_TAG: fn() -> String = || "<em>".to_string();
|
||||
pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "</em>".to_string();
|
||||
pub const DEFAULT_SEMANTIC_RATIO: fn() -> SemanticRatio = || SemanticRatio(0.5);
|
||||
|
||||
#[derive(Clone, Default, PartialEq, Deserr)]
|
||||
#[derive(Clone, Default, PartialEq, Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct SearchQuery {
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchQ>)]
|
||||
@ -62,8 +66,10 @@ pub struct SearchQuery {
|
||||
#[deserr(default, error = DeserrJsonError<InvalidHybridQuery>)]
|
||||
pub hybrid: Option<HybridQuery>,
|
||||
#[deserr(default = DEFAULT_SEARCH_OFFSET(), error = DeserrJsonError<InvalidSearchOffset>)]
|
||||
#[schema(default = DEFAULT_SEARCH_OFFSET)]
|
||||
pub offset: usize,
|
||||
#[deserr(default = DEFAULT_SEARCH_LIMIT(), error = DeserrJsonError<InvalidSearchLimit>)]
|
||||
#[schema(default = DEFAULT_SEARCH_LIMIT)]
|
||||
pub limit: usize,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchPage>)]
|
||||
pub page: Option<usize>,
|
||||
@ -75,15 +81,16 @@ pub struct SearchQuery {
|
||||
pub retrieve_vectors: bool,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToCrop>)]
|
||||
pub attributes_to_crop: Option<Vec<String>>,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchCropLength>, default = DEFAULT_CROP_LENGTH())]
|
||||
#[deserr(error = DeserrJsonError<InvalidSearchCropLength>, default = DEFAULT_CROP_LENGTH())]
|
||||
#[schema(default = DEFAULT_CROP_LENGTH)]
|
||||
pub crop_length: usize,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToHighlight>)]
|
||||
pub attributes_to_highlight: Option<HashSet<String>>,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchShowMatchesPosition>, default)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchShowMatchesPosition>)]
|
||||
pub show_matches_position: bool,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchShowRankingScore>, default)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchShowRankingScore>)]
|
||||
pub show_ranking_score: bool,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchShowRankingScoreDetails>, default)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchShowRankingScoreDetails>)]
|
||||
pub show_ranking_score_details: bool,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchFilter>)]
|
||||
pub filter: Option<Value>,
|
||||
@ -93,26 +100,28 @@ pub struct SearchQuery {
|
||||
pub distinct: Option<String>,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchFacets>)]
|
||||
pub facets: Option<Vec<String>>,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())]
|
||||
#[deserr(error = DeserrJsonError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())]
|
||||
#[schema(default = DEFAULT_HIGHLIGHT_PRE_TAG)]
|
||||
pub highlight_pre_tag: String,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())]
|
||||
#[deserr(error = DeserrJsonError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())]
|
||||
#[schema(default = DEFAULT_HIGHLIGHT_POST_TAG)]
|
||||
pub highlight_post_tag: String,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchCropMarker>, default = DEFAULT_CROP_MARKER())]
|
||||
#[deserr(error = DeserrJsonError<InvalidSearchCropMarker>, default = DEFAULT_CROP_MARKER())]
|
||||
#[schema(default = DEFAULT_CROP_MARKER)]
|
||||
pub crop_marker: String,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchMatchingStrategy>, default)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchMatchingStrategy>)]
|
||||
pub matching_strategy: MatchingStrategy,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToSearchOn>, default)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchAttributesToSearchOn>)]
|
||||
pub attributes_to_search_on: Option<Vec<String>>,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchRankingScoreThreshold>, default)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchRankingScoreThreshold>)]
|
||||
pub ranking_score_threshold: Option<RankingScoreThreshold>,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchLocales>, default)]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchLocales>)]
|
||||
pub locales: Option<Vec<Locale>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Deserr)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Deserr, ToSchema)]
|
||||
#[deserr(try_from(f64) = TryFrom::try_from -> InvalidSearchRankingScoreThreshold)]
|
||||
pub struct RankingScoreThreshold(f64);
|
||||
|
||||
impl std::convert::TryFrom<f64> for RankingScoreThreshold {
|
||||
type Error = InvalidSearchRankingScoreThreshold;
|
||||
|
||||
@ -266,10 +275,11 @@ impl fmt::Debug for SearchQuery {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Deserr)]
|
||||
#[derive(Debug, Clone, Default, PartialEq, Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError<InvalidHybridQuery>, rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct HybridQuery {
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSearchSemanticRatio>, default)]
|
||||
#[schema(value_type = f32, default)]
|
||||
pub semantic_ratio: SemanticRatio,
|
||||
#[deserr(error = DeserrJsonError<InvalidEmbedder>)]
|
||||
pub embedder: String,
|
||||
@ -381,8 +391,9 @@ impl SearchQuery {
|
||||
// This struct contains the fields of `SearchQuery` inline.
|
||||
// This is because neither deserr nor serde support `flatten` when using `deny_unknown_fields.
|
||||
// The `From<SearchQueryWithIndex>` implementation ensures both structs remain up to date.
|
||||
#[derive(Debug, Clone, PartialEq, Deserr)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct SearchQueryWithIndex {
|
||||
#[deserr(error = DeserrJsonError<InvalidIndexUid>, missing_field_error = DeserrJsonError::missing_index_uid)]
|
||||
pub index_uid: IndexUid,
|
||||
@ -530,10 +541,11 @@ impl SearchQueryWithIndex {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserr)]
|
||||
#[derive(Debug, Clone, PartialEq, Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct SimilarQuery {
|
||||
#[deserr(error = DeserrJsonError<InvalidSimilarId>)]
|
||||
#[schema(value_type = String)]
|
||||
pub id: ExternalDocumentId,
|
||||
#[deserr(default = DEFAULT_SEARCH_OFFSET(), error = DeserrJsonError<InvalidSimilarOffset>)]
|
||||
pub offset: usize,
|
||||
@ -552,6 +564,7 @@ pub struct SimilarQuery {
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSimilarShowRankingScoreDetails>, default)]
|
||||
pub show_ranking_score_details: bool,
|
||||
#[deserr(default, error = DeserrJsonError<InvalidSimilarRankingScoreThreshold>, default)]
|
||||
#[schema(value_type = f64)]
|
||||
pub ranking_score_threshold: Option<RankingScoreThresholdSimilar>,
|
||||
}
|
||||
|
||||
@ -587,7 +600,7 @@ impl TryFrom<Value> for ExternalDocumentId {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserr)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserr, ToSchema)]
|
||||
#[deserr(rename_all = camelCase)]
|
||||
pub enum MatchingStrategy {
|
||||
/// Remove query words from last to first
|
||||
@ -634,11 +647,13 @@ impl From<FacetValuesSort> for OrderBy {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq)]
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, ToSchema)]
|
||||
pub struct SearchHit {
|
||||
#[serde(flatten)]
|
||||
#[schema(additional_properties, inline, value_type = HashMap<String, Value>)]
|
||||
pub document: Document,
|
||||
#[serde(rename = "_formatted", skip_serializing_if = "Document::is_empty")]
|
||||
#[schema(additional_properties, value_type = HashMap<String, Value>)]
|
||||
pub formatted: Document,
|
||||
#[serde(rename = "_matchesPosition", skip_serializing_if = "Option::is_none")]
|
||||
pub matches_position: Option<MatchesPosition>,
|
||||
@ -648,8 +663,9 @@ pub struct SearchHit {
|
||||
pub ranking_score_details: Option<serde_json::Map<String, serde_json::Value>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Clone, PartialEq)]
|
||||
#[derive(Serialize, Clone, PartialEq, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct SearchResult {
|
||||
pub hits: Vec<SearchHit>,
|
||||
pub query: String,
|
||||
@ -657,6 +673,7 @@ pub struct SearchResult {
|
||||
#[serde(flatten)]
|
||||
pub hits_info: HitsInfo,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[schema(value_type = Option<BTreeMap<String, Value>>)]
|
||||
pub facet_distribution: Option<BTreeMap<String, IndexMap<String, u64>>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub facet_stats: Option<BTreeMap<String, FacetStats>>,
|
||||
@ -711,7 +728,7 @@ impl fmt::Debug for SearchResult {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SimilarResult {
|
||||
pub hits: Vec<SearchHit>,
|
||||
@ -721,24 +738,27 @@ pub struct SimilarResult {
|
||||
pub hits_info: HitsInfo,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct SearchResultWithIndex {
|
||||
pub index_uid: String,
|
||||
#[serde(flatten)]
|
||||
pub result: SearchResult,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
|
||||
#[serde(untagged)]
|
||||
pub enum HitsInfo {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
Pagination { hits_per_page: usize, page: usize, total_pages: usize, total_hits: usize },
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
OffsetLimit { limit: usize, offset: usize, estimated_total_hits: usize },
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq)]
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, ToSchema)]
|
||||
pub struct FacetStats {
|
||||
pub min: f64,
|
||||
pub max: f64,
|
||||
@ -1021,8 +1041,9 @@ pub fn perform_search(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize)]
|
||||
#[derive(Debug, Clone, Default, Serialize, ToSchema)]
|
||||
pub struct ComputedFacets {
|
||||
#[schema(value_type = BTreeMap<String, BTreeMap<String, u64>>)]
|
||||
pub distribution: BTreeMap<String, IndexMap<String, u64>>,
|
||||
pub stats: BTreeMap<String, FacetStats>,
|
||||
}
|
||||
|
@ -260,7 +260,7 @@ async fn distribution_shift() {
|
||||
|
||||
snapshot!(code, @"202 Accepted");
|
||||
let response = server.wait_task(response.uid()).await;
|
||||
snapshot!(response["details"], @r###"{"embedders":{"default":{"distribution":{"mean":0.998,"sigma":0.01}}}}"###);
|
||||
snapshot!(response["details"], @r#"{"embedders":{"default":{"distribution":{"mean":0.998,"sigma":0.01}}}}"#);
|
||||
|
||||
let (response, code) = index.search_post(search).await;
|
||||
snapshot!(code, @"200 OK");
|
||||
|
@ -285,7 +285,7 @@ async fn secrets_are_hidden_in_settings() {
|
||||
|
||||
let (response, code) = index.settings().await;
|
||||
meili_snap::snapshot!(code, @"200 OK");
|
||||
meili_snap::snapshot!(meili_snap::json_string!(response), @r###"
|
||||
meili_snap::snapshot!(meili_snap::json_string!(response), @r#"
|
||||
{
|
||||
"displayedAttributes": [
|
||||
"*"
|
||||
@ -346,11 +346,11 @@ async fn secrets_are_hidden_in_settings() {
|
||||
"facetSearch": true,
|
||||
"prefixSearch": "indexingTime"
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
|
||||
let (response, code) = server.get_task(settings_update_uid).await;
|
||||
meili_snap::snapshot!(code, @"200 OK");
|
||||
meili_snap::snapshot!(meili_snap::json_string!(response["details"]), @r###"
|
||||
meili_snap::snapshot!(meili_snap::json_string!(response["details"]), @r#"
|
||||
{
|
||||
"embedders": {
|
||||
"default": {
|
||||
@ -363,7 +363,7 @@ async fn secrets_are_hidden_in_settings() {
|
||||
}
|
||||
}
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -35,7 +35,7 @@ async fn retrieve_binary_quantize_status_in_the_settings() {
|
||||
|
||||
let (settings, code) = index.settings().await;
|
||||
snapshot!(code, @"200 OK");
|
||||
snapshot!(settings["embedders"]["manual"], @r###"{"source":"userProvided","dimensions":3}"###);
|
||||
snapshot!(settings["embedders"]["manual"], @r#"{"source":"userProvided","dimensions":3}"#);
|
||||
|
||||
let (response, code) = index
|
||||
.update_settings(json!({
|
||||
@ -53,7 +53,7 @@ async fn retrieve_binary_quantize_status_in_the_settings() {
|
||||
|
||||
let (settings, code) = index.settings().await;
|
||||
snapshot!(code, @"200 OK");
|
||||
snapshot!(settings["embedders"]["manual"], @r###"{"source":"userProvided","dimensions":3,"binaryQuantized":false}"###);
|
||||
snapshot!(settings["embedders"]["manual"], @r#"{"source":"userProvided","dimensions":3,"binaryQuantized":false}"#);
|
||||
|
||||
let (response, code) = index
|
||||
.update_settings(json!({
|
||||
@ -71,7 +71,7 @@ async fn retrieve_binary_quantize_status_in_the_settings() {
|
||||
|
||||
let (settings, code) = index.settings().await;
|
||||
snapshot!(code, @"200 OK");
|
||||
snapshot!(settings["embedders"]["manual"], @r###"{"source":"userProvided","dimensions":3,"binaryQuantized":true}"###);
|
||||
snapshot!(settings["embedders"]["manual"], @r#"{"source":"userProvided","dimensions":3,"binaryQuantized":true}"#);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@ -300,7 +300,7 @@ async fn try_to_disable_binary_quantization() {
|
||||
.await;
|
||||
snapshot!(code, @"202 Accepted");
|
||||
let ret = server.wait_task(response.uid()).await;
|
||||
snapshot!(ret, @r###"
|
||||
snapshot!(ret, @r#"
|
||||
{
|
||||
"uid": "[uid]",
|
||||
"batchUid": "[batch_uid]",
|
||||
@ -328,7 +328,7 @@ async fn try_to_disable_binary_quantization() {
|
||||
"startedAt": "[date]",
|
||||
"finishedAt": "[date]"
|
||||
}
|
||||
"###);
|
||||
"#);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -100,6 +100,7 @@ uell = "0.1.0"
|
||||
enum-iterator = "2.1.0"
|
||||
bbqueue = { git = "https://github.com/meilisearch/bbqueue" }
|
||||
flume = { version = "0.11.1", default-features = false }
|
||||
utoipa = { version = "5.0.2", features = ["non_strict_integers", "preserve_order", "uuid", "time", "openapi_extensions"] }
|
||||
|
||||
[dev-dependencies]
|
||||
mimalloc = { version = "0.1.43", default-features = false }
|
||||
|
@ -2,6 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use charabia::Language;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use crate::fields_ids_map::FieldsIdsMap;
|
||||
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 ends with `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 attribute_patterns: Vec<String>,
|
||||
#[schema(value_type = Vec<String>)]
|
||||
pub locales: Vec<Language>,
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ use std::sync::atomic::{AtomicU32, Ordering};
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use serde::Serialize;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub trait Step: 'static + Send + Sync {
|
||||
fn name(&self) -> Cow<'static, str>;
|
||||
@ -136,15 +137,17 @@ macro_rules! make_atomic_progress {
|
||||
make_atomic_progress!(Document alias AtomicDocumentStep => "document" );
|
||||
make_atomic_progress!(Payload alias AtomicPayloadStep => "payload" );
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[derive(Debug, Serialize, Clone, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct ProgressView {
|
||||
pub steps: Vec<ProgressStepView>,
|
||||
pub percentage: f32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[derive(Debug, Serialize, Clone, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[schema(rename_all = "camelCase")]
|
||||
pub struct ProgressStepView {
|
||||
pub current_step: Cow<'static, str>,
|
||||
pub finished: u32,
|
||||
|
@ -13,6 +13,7 @@ use matching_words::{MatchType, PartialMatch};
|
||||
use r#match::{Match, MatchPosition};
|
||||
use serde::Serialize;
|
||||
use simple_token_kind::SimpleTokenKind;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
const DEFAULT_CROP_MARKER: &str = "…";
|
||||
const DEFAULT_HIGHLIGHT_PREFIX: &str = "<em>";
|
||||
@ -100,7 +101,7 @@ impl FormatOptions {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Serialize, Debug, Clone, PartialEq, Eq, ToSchema)]
|
||||
pub struct MatchBounds {
|
||||
pub start: usize,
|
||||
pub length: usize,
|
||||
|
@ -9,6 +9,7 @@ use heed::{RoTxn, RwTxn, Unspecified};
|
||||
use ordered_float::OrderedFloat;
|
||||
use roaring::RoaringBitmap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use self::error::{EmbedError, NewEmbedderError};
|
||||
use crate::prompt::{Prompt, PromptData};
|
||||
@ -710,18 +711,20 @@ impl Embedder {
|
||||
///
|
||||
/// The intended use is to make the similarity score more comparable to the regular ranking score.
|
||||
/// This allows to correct effects where results are too "packed" around a certain value.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize, ToSchema)]
|
||||
#[serde(from = "DistributionShiftSerializable")]
|
||||
#[serde(into = "DistributionShiftSerializable")]
|
||||
pub struct DistributionShift {
|
||||
/// Value where the results are "packed".
|
||||
///
|
||||
/// Similarity scores are translated so that they are packed around 0.5 instead
|
||||
#[schema(value_type = f32)]
|
||||
pub current_mean: OrderedFloat<f32>,
|
||||
|
||||
/// standard deviation of a similarity score.
|
||||
///
|
||||
/// Set below 0.4 to make the results less packed around the mean, and above 0.4 to make them more packed.
|
||||
#[schema(value_type = f32)]
|
||||
pub current_sigma: OrderedFloat<f32>,
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,7 @@ use std::num::NonZeroUsize;
|
||||
use deserr::Deserr;
|
||||
use roaring::RoaringBitmap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
use super::{ollama, openai, DistributionShift};
|
||||
use crate::prompt::{default_max_bytes, PromptData};
|
||||
@ -11,48 +12,61 @@ use crate::update::Setting;
|
||||
use crate::vector::EmbeddingConfig;
|
||||
use crate::UserError;
|
||||
|
||||
#[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")]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
pub struct EmbeddingSettings {
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<EmbedderSource>)]
|
||||
pub source: Setting<EmbedderSource>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub model: Setting<String>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub revision: Setting<String>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub api_key: Setting<String>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub dimensions: Setting<usize>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<bool>)]
|
||||
pub binary_quantized: Setting<bool>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<bool>)]
|
||||
pub document_template: Setting<String>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<usize>)]
|
||||
pub document_template_max_bytes: Setting<usize>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<String>)]
|
||||
pub url: Setting<String>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<serde_json::Value>)]
|
||||
pub request: Setting<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<serde_json::Value>)]
|
||||
pub response: Setting<serde_json::Value>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<BTreeMap<String, String>>)]
|
||||
pub headers: Setting<BTreeMap<String, String>>,
|
||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||
#[deserr(default)]
|
||||
#[schema(value_type = Option<DistributionShift>)]
|
||||
pub distribution: Setting<DistributionShift>,
|
||||
}
|
||||
|
||||
@ -539,7 +553,7 @@ impl EmbeddingSettings {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)]
|
||||
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq, Deserr, ToSchema)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||
pub enum EmbedderSource {
|
||||
|
Loading…
x
Reference in New Issue
Block a user