mirror of
https://github.com/meilisearch/MeiliSearch
synced 2025-07-04 04:17:10 +02:00
Implements the get and delete tasks route
This commit is contained in:
parent
f00a285a6d
commit
742d0ee531
25 changed files with 1787 additions and 85 deletions
|
@ -13,6 +13,7 @@ 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;
|
||||
|
@ -21,6 +22,20 @@ 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 +50,52 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
/// Create an API Key
|
||||
///
|
||||
/// Create an API Key.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/",
|
||||
tag = "Keys",
|
||||
security(("Bearer" = ["keys.create", "keys.*", "*"])),
|
||||
request_body = CreateApiKey,
|
||||
responses(
|
||||
(status = 202, description = "Key has been created", body = KeyView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
|
||||
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
|
||||
"name": "Indexing Products API key",
|
||||
"description": null,
|
||||
"actions": [
|
||||
"documents.add"
|
||||
],
|
||||
"indexes": [
|
||||
"products"
|
||||
],
|
||||
"expiresAt": "2021-11-13T00:00:00Z",
|
||||
"createdAt": "2021-11-12T10:00:00Z",
|
||||
"updatedAt": "2021-11-12T10:00:00Z"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
|
||||
"code": "missing_master_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_master_key"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn create_api_key(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_CREATE }>, Data<AuthController>>,
|
||||
body: AwebJson<CreateApiKey, DeserrJsonError>,
|
||||
|
@ -51,11 +112,14 @@ 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 {
|
||||
#[into_params(value_type = usize, default = 0)]
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidApiKeyOffset>)]
|
||||
pub offset: Param<usize>,
|
||||
#[into_params(value_type = usize, default = PAGINATION_DEFAULT_LIMIT)]
|
||||
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidApiKeyLimit>)]
|
||||
pub limit: Param<usize>,
|
||||
}
|
||||
|
@ -66,6 +130,60 @@ impl ListApiKeys {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// Get API Keys
|
||||
///
|
||||
/// List all API Keys
|
||||
/// TODO: Tamo fix the return type
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/",
|
||||
tag = "Keys",
|
||||
security(("Bearer" = ["keys.get", "keys.*", "*"])),
|
||||
params(ListApiKeys),
|
||||
responses(
|
||||
(status = 202, description = "List of keys", body = serde_json::Value, content_type = "application/json", example = json!(
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
|
||||
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
|
||||
"name": "An API Key",
|
||||
"description": null,
|
||||
"actions": [
|
||||
"documents.add"
|
||||
],
|
||||
"indexes": [
|
||||
"movies"
|
||||
],
|
||||
"expiresAt": "2022-11-12T10:00:00Z",
|
||||
"createdAt": "2021-11-12T10:00:00Z",
|
||||
"updatedAt": "2021-11-12T10:00:00Z"
|
||||
}
|
||||
],
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"total": 1
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
|
||||
"code": "missing_master_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_master_key"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn list_api_keys(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
|
||||
list_api_keys: AwebQueryParameter<ListApiKeys, DeserrQueryParamError>,
|
||||
|
@ -84,6 +202,52 @@ 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 = "/{key}",
|
||||
tag = "Keys",
|
||||
security(("Bearer" = ["keys.get", "keys.*", "*"])),
|
||||
params(("uidOrKey" = String, Path, format = Password, example = "7b198a7f-52a0-4188-8762-9ad93cd608b2", description = "The `uid` or `key` field of an existing API key", nullable = false)),
|
||||
responses(
|
||||
(status = 200, description = "The key is returned", body = KeyView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
|
||||
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
|
||||
"name": "An API Key",
|
||||
"description": null,
|
||||
"actions": [
|
||||
"documents.add"
|
||||
],
|
||||
"indexes": [
|
||||
"movies"
|
||||
],
|
||||
"expiresAt": "2022-11-12T10:00:00Z",
|
||||
"createdAt": "2021-11-12T10:00:00Z",
|
||||
"updatedAt": "2021-11-12T10:00:00Z"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
|
||||
"code": "missing_master_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_master_key"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn get_api_key(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_GET }>, Data<AuthController>>,
|
||||
path: web::Path<AuthParam>,
|
||||
|
@ -103,6 +267,55 @@ pub async fn get_api_key(
|
|||
Ok(HttpResponse::Ok().json(res))
|
||||
}
|
||||
|
||||
|
||||
/// Update an API Key
|
||||
///
|
||||
/// Update an API key from its `uid` or its `key` field.
|
||||
/// Only the `name` and `description` of the api key can be updated.
|
||||
/// If there is an issue with the `key` or `uid` of a key, then you must recreate one from scratch.
|
||||
#[utoipa::path(
|
||||
patch,
|
||||
path = "/{key}",
|
||||
tag = "Keys",
|
||||
security(("Bearer" = ["keys.update", "keys.*", "*"])),
|
||||
params(("uidOrKey" = String, Path, format = Password, example = "7b198a7f-52a0-4188-8762-9ad93cd608b2", description = "The `uid` or `key` field of an existing API key", nullable = false)),
|
||||
request_body = PatchApiKey,
|
||||
responses(
|
||||
(status = 200, description = "The key have been updated", body = KeyView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"uid": "01b4bc42-eb33-4041-b481-254d00cce834",
|
||||
"key": "d0552b41536279a0ad88bd595327b96f01176a60c2243e906c52ac02375f9bc4",
|
||||
"name": "An API Key",
|
||||
"description": null,
|
||||
"actions": [
|
||||
"documents.add"
|
||||
],
|
||||
"indexes": [
|
||||
"movies"
|
||||
],
|
||||
"expiresAt": "2022-11-12T10:00:00Z",
|
||||
"createdAt": "2021-11-12T10:00:00Z",
|
||||
"updatedAt": "2021-11-12T10:00:00Z"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
|
||||
"code": "missing_master_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_master_key"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn patch_api_key(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_UPDATE }>, Data<AuthController>>,
|
||||
body: AwebJson<PatchApiKey, DeserrJsonError>,
|
||||
|
@ -123,6 +336,39 @@ pub async fn patch_api_key(
|
|||
Ok(HttpResponse::Ok().json(res))
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// Update an API Key
|
||||
///
|
||||
/// Update an API key from its `uid` or its `key` field.
|
||||
/// Only the `name` and `description` of the api key can be updated.
|
||||
/// If there is an issue with the `key` or `uid` of a key, then you must recreate one from scratch.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/{key}",
|
||||
tag = "Keys",
|
||||
security(("Bearer" = ["keys.delete", "keys.*", "*"])),
|
||||
params(("uidOrKey" = String, Path, format = Password, example = "7b198a7f-52a0-4188-8762-9ad93cd608b2", description = "The `uid` or `key` field of an existing API key", nullable = false)),
|
||||
responses(
|
||||
(status = NO_CONTENT, description = "The key have been removed"),
|
||||
(status = 401, description = "The route has been hit on an unprotected instance", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Meilisearch is running without a master key. To access this API endpoint, you must have set a master key at launch.",
|
||||
"code": "missing_master_key",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_master_key"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn delete_api_key(
|
||||
auth_controller: GuardedData<ActionPolicy<{ actions::KEYS_DELETE }>, Data<AuthController>>,
|
||||
path: web::Path<AuthParam>,
|
||||
|
@ -144,19 +390,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,
|
||||
}
|
||||
|
|
|
@ -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,61 @@ 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>>,
|
||||
|
|
|
@ -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_documents, replace_documents, update_documents, clear_all_documents, delete_documents_batch),
|
||||
tags(
|
||||
(
|
||||
name = "Documents",
|
||||
description = "Documents are objects composed of fields that can store any type of data. Each field contains an attribute and its associated value. Documents are stored inside [indexes](https://www.meilisearch.com/docs/learn/getting_started/indexes).",
|
||||
external_docs(url = "https://www.meilisearch.com/docs/learn/getting_started/documents"),
|
||||
),
|
||||
),
|
||||
)]
|
||||
pub struct DocumentsApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
|
@ -286,17 +300,23 @@ pub struct BrowseQueryGet {
|
|||
filter: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserr)]
|
||||
#[derive(Debug, Deserr, IntoParams, 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, example = "popularity > 1000")]
|
||||
#[deserr(default, error = DeserrJsonError<InvalidDocumentFilter>)]
|
||||
filter: Option<Value>,
|
||||
}
|
||||
|
@ -326,6 +346,62 @@ 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",
|
||||
tags = ["Indexes", "Documents"],
|
||||
security(("Bearer" = ["documents.get", "documents.*", "*"])),
|
||||
params(
|
||||
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
// Here we can use the post version of the browse query since it contains the exact same parameter
|
||||
BrowseQuery
|
||||
),
|
||||
responses(
|
||||
// body = PaginationView<Document>
|
||||
(status = 200, description = "The documents are returned", body = serde_json::Value, content_type = "application/json", example = json!(
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"id": 25684,
|
||||
"title": "American Ninja 5",
|
||||
"poster": "https://image.tmdb.org/t/p/w1280/iuAQVI4mvjI83wnirpD8GVNRVuY.jpg",
|
||||
"overview": "When a scientists daughter is kidnapped, American Ninja, attempts to find her, but this time he teams up with a youngster he has trained in the ways of the ninja.",
|
||||
"release_date": 725846400
|
||||
},
|
||||
{
|
||||
"id": 45881,
|
||||
"title": "The Bridge of San Luis Rey",
|
||||
"poster": "https://image.tmdb.org/t/p/w500/4X7quIcdkc24Cveg5XdpfRqxtYA.jpg",
|
||||
"overview": "The Bridge of San Luis Rey is American author Thornton Wilder's second novel, first published in 1927 to worldwide acclaim. It tells the story of several interrelated people who die in the collapse of an Inca rope-fiber suspension bridge in Peru, and the events that lead up to their being on the bridge.[ A friar who has witnessed the tragic accident then goes about inquiring into the lives of the victims, seeking some sort of cosmic answer to the question of why each had to die. The novel won the Pulitzer Prize in 1928.",
|
||||
"release_date": 1072915200
|
||||
}
|
||||
],
|
||||
"limit": 20,
|
||||
"offset": 0,
|
||||
"total": 2
|
||||
}
|
||||
)),
|
||||
(status = 404, description = "Index not found", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Index `movies` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn get_documents(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_GET }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
|
@ -396,11 +472,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(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 +533,51 @@ impl<Method: AggregateMethod> Aggregate for DocumentsAggregator<Method> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Add or replace documents
|
||||
///
|
||||
/// Add a list of documents or replace them if they already exist.
|
||||
///
|
||||
/// If you send an already existing document (same id) the whole existing document will be overwritten by the new document. Fields previously in the document not present in the new document are removed.
|
||||
///
|
||||
/// For a partial update of the document see Add or update documents route.
|
||||
/// > info
|
||||
/// > If the provided index does not exist, it will be created.
|
||||
/// > info
|
||||
/// > Use the reserved `_geo` object to add geo coordinates to a document. `_geo` is an object made of `lat` and `lng` field.
|
||||
/// >
|
||||
/// > When the vectorStore feature is enabled you can use the reserved `_vectors` field in your documents.
|
||||
/// > It can accept an array of floats, multiple arrays of floats in an outer array or an object.
|
||||
/// > This object accepts keys corresponding to the different embedders defined your index settings.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/{indexUid}/documents",
|
||||
tags = ["Indexes", "Documents"],
|
||||
security(("Bearer" = ["documents.add", "documents.*", "*"])),
|
||||
params(
|
||||
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
// Here we can use the post version of the browse query since it contains the exact same parameter
|
||||
UpdateDocumentsQuery,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"taskUid": 147,
|
||||
"indexUid": null,
|
||||
"status": "enqueued",
|
||||
"type": "documentAdditionOrUpdate",
|
||||
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn replace_documents(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
|
@ -508,6 +635,49 @@ 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",
|
||||
tags = ["Indexes", "Documents"],
|
||||
security(("Bearer" = ["documents.add", "documents.*", "*"])),
|
||||
params(
|
||||
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
// Here we can use the post version of the browse query since it contains the exact same parameter
|
||||
UpdateDocumentsQuery,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"taskUid": 147,
|
||||
"indexUid": null,
|
||||
"status": "enqueued",
|
||||
"type": "documentAdditionOrUpdate",
|
||||
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn update_documents(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_ADD }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
|
@ -742,6 +912,38 @@ async fn copy_body_to_file(
|
|||
Ok(read_file)
|
||||
}
|
||||
|
||||
/// Delete documents
|
||||
///
|
||||
/// Delete a selection of documents based on array of document id's.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/{indexUid}/documents",
|
||||
tags = ["Indexes", "Documents"],
|
||||
security(("Bearer" = ["documents.delete", "documents.*", "*"])),
|
||||
params(
|
||||
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
),
|
||||
// TODO: how to return an array of strings
|
||||
responses(
|
||||
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"taskUid": 147,
|
||||
"indexUid": null,
|
||||
"status": "enqueued",
|
||||
"type": "documentAdditionOrUpdate",
|
||||
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn delete_documents_batch(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
|
@ -936,6 +1138,38 @@ 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",
|
||||
tags = ["Indexes", "Documents"],
|
||||
security(("Bearer" = ["documents.delete", "documents.*", "*"])),
|
||||
params(
|
||||
("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false),
|
||||
UpdateDocumentsQuery,
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"taskUid": 147,
|
||||
"indexUid": null,
|
||||
"status": "enqueued",
|
||||
"type": "documentDeletion",
|
||||
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn clear_all_documents(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::DOCUMENTS_DELETE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
|
|
|
@ -16,6 +16,7 @@ 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 crate::analytics::{Aggregate, Analytics};
|
||||
|
@ -36,6 +37,22 @@ mod settings_analytics;
|
|||
pub mod similar;
|
||||
mod similar_analytics;
|
||||
|
||||
#[derive(OpenApi)]
|
||||
#[openapi(
|
||||
nest(
|
||||
(path = "/", api = documents::DocumentsApi),
|
||||
),
|
||||
paths(list_indexes, create_index, get_index, update_index, delete_index, get_index_stats),
|
||||
tags(
|
||||
(
|
||||
name = "Indexes",
|
||||
description = "An index is an entity that gathers a set of [documents](https://www.meilisearch.com/docs/learn/getting_started/documents) with its own [settings](https://www.meilisearch.com/docs/reference/api/settings). Learn more about indexes.",
|
||||
external_docs(url = "https://www.meilisearch.com/docs/reference/api/indexes"),
|
||||
),
|
||||
),
|
||||
)]
|
||||
pub struct IndexesApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::resource("")
|
||||
|
@ -59,14 +76,18 @@ pub fn configure(cfg: &mut web::ServiceConfig) {
|
|||
);
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Clone)]
|
||||
#[derive(Debug, Serialize, Clone, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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 +105,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")]
|
||||
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 = serde_json::Value, content_type = "application/json", example = json!(
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"uid": "movies",
|
||||
"primaryKey": "movie_id",
|
||||
"createdAt": "2019-11-20T09:40:33.711324Z",
|
||||
"updatedAt": "2019-11-20T09:40:33.711324Z"
|
||||
}
|
||||
],
|
||||
"limit": 1,
|
||||
"offset": 0,
|
||||
"total": 1
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn list_indexes(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_GET }>, Data<IndexScheduler>>,
|
||||
paginate: AwebQueryParameter<ListIndexes, DeserrQueryParamError>,
|
||||
|
@ -121,11 +183,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 +216,35 @@ impl Aggregate for IndexCreatedAggregate {
|
|||
}
|
||||
}
|
||||
|
||||
/// Create index
|
||||
///
|
||||
/// Create an index.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/",
|
||||
tag = "Indexes",
|
||||
security(("Bearer" = ["indexes.create", "indexes.*", "*"])),
|
||||
request_body = IndexCreateRequest,
|
||||
responses(
|
||||
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"taskUid": 147,
|
||||
"indexUid": "movies",
|
||||
"status": "enqueued",
|
||||
"type": "indexCreation",
|
||||
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn create_index(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_CREATE }>, Data<IndexScheduler>>,
|
||||
body: AwebJson<IndexCreateRequest, DeserrJsonError>,
|
||||
|
@ -198,13 +294,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 +362,48 @@ 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"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
>>>>>>> 0f289a437 (Implements the get and delete tasks route):meilisearch/src/routes/indexes/mod.rs
|
||||
pub async fn update_index(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::INDEXES_UPDATE }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<String>,
|
||||
|
@ -269,6 +436,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 +485,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 +507,44 @@ impl From<index_scheduler::IndexStats> for IndexStats {
|
|||
}
|
||||
}
|
||||
|
||||
/// Get stats of index
|
||||
///
|
||||
/// Get the stats of an index.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/{indexUid}/stats",
|
||||
tags = ["Indexes", "Stats"],
|
||||
security(("Bearer" = ["stats.get", "stats.*", "*"])),
|
||||
params(("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false)),
|
||||
responses(
|
||||
(status = OK, description = "The stats of the index", body = IndexStats, content_type = "application/json", example = json!(
|
||||
{
|
||||
"numberOfDocuments": 10,
|
||||
"isIndexing": true,
|
||||
"fieldDistribution": {
|
||||
"genre": 10,
|
||||
"author": 9
|
||||
}
|
||||
}
|
||||
)),
|
||||
(status = 404, description = "Index not found", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Index `movies` not found.",
|
||||
"code": "index_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#index_not_found"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn get_index_stats(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::STATS_GET }>, Data<IndexScheduler>>,
|
||||
index_uid: web::Path<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,20 @@ 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 +49,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 +103,26 @@ impl MergeWithError<MyParseError> for DeserrJsonError<BadRequest> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserr)]
|
||||
#[derive(Debug, Deserr, ToSchema)]
|
||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields, validate = validate_get_logs -> DeserrJsonError<InvalidSettingsTypoTolerance>)]
|
||||
#[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 +278,46 @@ fn entry_stream(
|
|||
)
|
||||
}
|
||||
|
||||
/// Retrieve logs
|
||||
///
|
||||
/// Stream logs over HTTP. The format of the logs depends on the configuration specified in the payload.
|
||||
/// The logs are sent as multi-part, and the stream never stops, so make sure your clients correctly handle that.
|
||||
/// To make the server stop sending you logs, you can call the `DELETE /logs/stream` route.
|
||||
///
|
||||
/// There can only be one listener at a timeand an error will be returned if you call this route while it's being used by another client.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/stream",
|
||||
tag = "Logs",
|
||||
security(("Bearer" = ["metrics.get", "metrics.*", "*"])),
|
||||
request_body = GetLogs,
|
||||
responses(
|
||||
(status = OK, description = "Logs are being returned", body = String, content_type = "application/json", example = json!(
|
||||
r#"
|
||||
2024-10-08T13:35:02.643750Z WARN HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=400 error=Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625}: tracing_actix_web::middleware: Error encountered while processing the incoming HTTP request: ResponseError { code: 400, message: "Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625", error_code: "feature_not_enabled", error_type: "invalid_request", error_link: "https://docs.meilisearch.com/errors#feature_not_enabled" }
|
||||
2024-10-08T13:35:02.644191Z INFO HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=400 error=Getting metrics requires enabling the `metrics` experimental feature. See https://github.com/meilisearch/product/discussions/625}: meilisearch: close time.busy=1.66ms time.idle=658µs
|
||||
2024-10-08T13:35:18.564152Z INFO HTTP request{method=PATCH host="localhost:7700" route=/experimental-features query_parameters= user_agent=curl/8.6.0 status_code=200}: meilisearch: close time.busy=1.17ms time.idle=127µs
|
||||
2024-10-08T13:35:23.094987Z INFO HTTP request{method=GET host="localhost:7700" route=/metrics query_parameters= user_agent=HTTPie/3.2.3 status_code=200}: meilisearch: close time.busy=2.12ms time.idle=595µs
|
||||
"#
|
||||
)),
|
||||
(status = 400, description = "The route is already being used", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The `/logs/stream` route is currently in use by someone else.",
|
||||
"code": "bad_request",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#bad_request"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn get_logs(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>,
|
||||
logs: Data<LogRouteHandle>,
|
||||
|
@ -280,6 +350,27 @@ pub async fn get_logs(
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/// Stop retrieving logs
|
||||
///
|
||||
/// Call this route to make the engine stops sending logs through the `POST /logs/stream` route.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
path = "/stream",
|
||||
tag = "Logs",
|
||||
security(("Bearer" = ["metrics.get", "metrics.*", "*"])),
|
||||
responses(
|
||||
(status = NO_CONTENT, description = "Logs are being returned"),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn cancel_logs(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::METRICS_GET }>, Data<IndexScheduler>>,
|
||||
logs: Data<LogRouteHandle>,
|
||||
|
@ -293,13 +384,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,12 +1,17 @@
|
|||
use crate::extractors::authentication::policies::ActionPolicy;
|
||||
use crate::extractors::authentication::{AuthenticationError, GuardedData};
|
||||
use crate::routes::create_all_stats;
|
||||
use crate::search_queue::SearchQueue;
|
||||
use actix_web::http::header;
|
||||
use actix_web::web::{self, Data};
|
||||
use actix_web::HttpResponse;
|
||||
use index_scheduler::{IndexScheduler, Query};
|
||||
use index_scheduler::IndexScheduler;
|
||||
use meilisearch_auth::AuthController;
|
||||
use meilisearch_types::error::ResponseError;
|
||||
use meilisearch_types::keys::actions;
|
||||
use meilisearch_types::tasks::Status;
|
||||
use prometheus::{Encoder, TextEncoder};
|
||||
use utoipa::OpenApi;
|
||||
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::extractors::authentication::policies::ActionPolicy;
|
||||
|
@ -14,10 +19,100 @@ 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>,
|
||||
|
|
|
@ -1,20 +1,38 @@
|
|||
use std::collections::BTreeMap;
|
||||
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use index_scheduler::IndexScheduler;
|
||||
use meilisearch_auth::AuthController;
|
||||
use meilisearch_types::error::{Code, ResponseError};
|
||||
use meilisearch_types::settings::{Settings, Unchecked};
|
||||
use meilisearch_types::tasks::{Kind, Status, Task, TaskId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::debug;
|
||||
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::GuardedData;
|
||||
use crate::search_queue::SearchQueue;
|
||||
use crate::Opt;
|
||||
use actix_web::web::Data;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use index_scheduler::IndexScheduler;
|
||||
use meilisearch_auth::AuthController;
|
||||
use meilisearch_types::error::{Code, ErrorType, ResponseError};
|
||||
use meilisearch_types::index_uid::IndexUid;
|
||||
use meilisearch_types::keys::CreateApiKey;
|
||||
use meilisearch_types::settings::{
|
||||
Checked, FacetingSettings, MinWordSizeTyposSetting, PaginationSettings, Settings, TypoSettings,
|
||||
Unchecked,
|
||||
};
|
||||
use meilisearch_types::task_view::{DetailsView, TaskView};
|
||||
use meilisearch_types::tasks::{Kind, Status, Task, TaskId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
use tracing::debug;
|
||||
use utoipa::{OpenApi, ToSchema};
|
||||
use utoipa_rapidoc::RapiDoc;
|
||||
use utoipa_redoc::{Redoc, Servable};
|
||||
use utoipa_scalar::{Scalar, Servable as ScalarServable};
|
||||
|
||||
use self::api_key::KeyView;
|
||||
use self::indexes::documents::BrowseQuery;
|
||||
use self::indexes::{IndexCreateRequest, IndexStats, UpdateIndexRequest};
|
||||
use self::logs::GetLogs;
|
||||
use self::logs::LogMode;
|
||||
use self::logs::UpdateStderrLogs;
|
||||
use self::open_api_utils::OpenApiAuth;
|
||||
use self::tasks::AllTasks;
|
||||
|
||||
const PAGINATION_DEFAULT_LIMIT: usize = 20;
|
||||
|
||||
|
@ -27,24 +45,50 @@ 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 = "/indexes", api = indexes::IndexesApi),
|
||||
(path = "/snapshots", api = snapshot::SnapshotApi),
|
||||
(path = "/dumps", api = dump::DumpApi),
|
||||
(path = "/keys", api = api_key::ApiKeyApi),
|
||||
(path = "/metrics", api = metrics::MetricApi),
|
||||
(path = "/logs", api = logs::LogsApi),
|
||||
),
|
||||
paths(get_health, get_version, get_stats),
|
||||
tags(
|
||||
(name = "Stats", description = "Stats gives extended information and metrics about indexes and the Meilisearch database."),
|
||||
),
|
||||
modifiers(&OpenApiAuth),
|
||||
components(schemas(BrowseQuery, UpdateIndexRequest, IndexUid, IndexCreateRequest, KeyView, Action, CreateApiKey, UpdateStderrLogs, LogMode, GetLogs, IndexStats, Stats, HealthStatus, HealthResponse, VersionResponse, Code, ErrorType, AllTasks, TaskView, Status, DetailsView, ResponseError, Settings<Unchecked>, Settings<Checked>, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings, SummarizedTaskView, Kind))
|
||||
)]
|
||||
pub struct MeilisearchApi;
|
||||
|
||||
pub fn configure(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(web::scope("/tasks").configure(tasks::configure))
|
||||
.service(web::scope("/batches").configure(batches::configure))
|
||||
.service(web::resource("/health").route(web::get().to(get_health)))
|
||||
.service(web::scope("/logs").configure(logs::configure))
|
||||
.service(web::scope("/keys").configure(api_key::configure))
|
||||
.service(web::scope("/dumps").configure(dump::configure))
|
||||
.service(web::scope("/snapshots").configure(snapshot::configure))
|
||||
.service(web::resource("/stats").route(web::get().to(get_stats)))
|
||||
.service(web::resource("/version").route(web::get().to(get_version)))
|
||||
.service(web::scope("/indexes").configure(indexes::configure))
|
||||
.service(web::scope("/multi-search").configure(multi_search::configure))
|
||||
.service(web::scope("/swap-indexes").configure(swap_indexes::configure))
|
||||
.service(web::scope("/metrics").configure(metrics::configure))
|
||||
let openapi = MeilisearchApi::openapi();
|
||||
|
||||
cfg.service(web::scope("/tasks").configure(tasks::configure)) // done
|
||||
.service(web::scope("/batches").configure(batches::configure)) // TODO
|
||||
.service(Scalar::with_url("/scalar", openapi.clone())) // done
|
||||
.service(RapiDoc::with_openapi("/api-docs/openapi.json", openapi.clone()).path("/rapidoc")) // done
|
||||
.service(Redoc::with_url("/redoc", openapi)) // done
|
||||
.service(web::resource("/health").route(web::get().to(get_health))) // done
|
||||
.service(web::scope("/logs").configure(logs::configure)) // done
|
||||
.service(web::scope("/keys").configure(api_key::configure)) // done
|
||||
.service(web::scope("/dumps").configure(dump::configure)) // done
|
||||
.service(web::scope("/snapshots").configure(snapshot::configure)) // done
|
||||
.service(web::resource("/stats").route(web::get().to(get_stats))) // done
|
||||
.service(web::resource("/version").route(web::get().to(get_version))) // done
|
||||
.service(web::scope("/indexes").configure(indexes::configure)) // WIP
|
||||
.service(web::scope("/multi-search").configure(multi_search::configure)) // TODO
|
||||
.service(web::scope("/swap-indexes").configure(swap_indexes::configure)) // TODO
|
||||
.service(web::scope("/metrics").configure(metrics::configure)) // done
|
||||
.service(web::scope("/experimental-features").configure(features::configure));
|
||||
}
|
||||
|
||||
|
@ -98,14 +142,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,
|
||||
}
|
||||
|
@ -128,6 +178,7 @@ pub struct Pagination {
|
|||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PaginationView<T> {
|
||||
pub results: Vec<T>,
|
||||
pub offset: usize,
|
||||
|
@ -283,17 +334,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 +433,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 +489,35 @@ async fn get_version(
|
|||
})
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HealthResponse {
|
||||
/// The status of the instance.
|
||||
status: HealthStatus,
|
||||
}
|
||||
|
||||
#[derive(Default, Serialize, ToSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
enum HealthStatus {
|
||||
#[default]
|
||||
Available,
|
||||
}
|
||||
|
||||
/// Get Health
|
||||
///
|
||||
/// The health check endpoint enables you to periodically test the health of your Meilisearch instance.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
path = "/health",
|
||||
tag = "Health",
|
||||
responses(
|
||||
(status = 200, description = "Instance is healthy", body = HealthResponse, content_type = "application/json", example = json!(
|
||||
{
|
||||
"status": "available"
|
||||
}
|
||||
)),
|
||||
)
|
||||
)]
|
||||
pub async fn get_health(
|
||||
index_scheduler: Data<IndexScheduler>,
|
||||
auth_controller: Data<AuthController>,
|
||||
|
@ -379,5 +527,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()))
|
||||
}
|
||||
|
|
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,56 @@ 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,
|
||||
|
|
|
@ -17,14 +17,29 @@ 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};
|
||||
use crate::analytics::Analytics;
|
||||
use super::{get_task_id, is_dry_run, SummarizedTaskView};
|
||||
use crate::analytics::Analytics;
|
||||
use crate::extractors::authentication::policies::*;
|
||||
use crate::extractors::authentication::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 +50,66 @@ 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 {
|
||||
#[deserr(default = Param(PAGINATION_DEFAULT_LIMIT as u32), error = DeserrQueryParamError<InvalidTaskLimit>)]
|
||||
/// Maximum number of results to return.
|
||||
#[deserr(default = Param(DEFAULT_LIMIT), error = DeserrQueryParamError<InvalidTaskLimit>)]
|
||||
#[param(required = false, value_type = u32, example = 12, default = json!(DEFAULT_LIMIT))]
|
||||
pub limit: Param<u32>,
|
||||
/// 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>>,
|
||||
|
||||
#[deserr(default, error = DeserrQueryParamError<InvalidBatchUids>)]
|
||||
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 +157,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 +291,51 @@ impl<Method: AggregateMethod + 'static> Aggregate for TaskFilterAnalytics<Method
|
|||
}
|
||||
}
|
||||
|
||||
/// Cancel tasks
|
||||
///
|
||||
/// Cancel enqueued and/or processing [tasks](https://www.meilisearch.com/docs/learn/async/asynchronous_operations)
|
||||
#[utoipa::path(
|
||||
post,
|
||||
path = "/cancel",
|
||||
tag = "Tasks",
|
||||
security(("Bearer" = ["tasks.cancel", "tasks.*", "*"])),
|
||||
params(TaskDeletionOrCancelationQuery),
|
||||
responses(
|
||||
(status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!(
|
||||
{
|
||||
"taskUid": 147,
|
||||
"indexUid": null,
|
||||
"status": "enqueued",
|
||||
"type": "taskCancelation",
|
||||
"enqueuedAt": "2024-08-08T17:05:55.791772Z"
|
||||
}
|
||||
)),
|
||||
(status = 400, description = "A filter is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Query parameters to filter the tasks to cancel are missing. Available query parameters are: `uids`, `indexUids`, `statuses`, `types`, `canceledBy`, `beforeEnqueuedAt`, `afterEnqueuedAt`, `beforeStartedAt`, `afterStartedAt`, `beforeFinishedAt`, `afterFinishedAt`.",
|
||||
"code": "missing_task_filters",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_task_filters"
|
||||
}
|
||||
)),
|
||||
(status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "The Authorization header is missing. It must use the bearer authorization method.",
|
||||
"code": "missing_authorization_header",
|
||||
"type": "auth",
|
||||
"link": "https://docs.meilisearch.com/errors#missing_authorization_header"
|
||||
}
|
||||
)),
|
||||
(status = 404, description = "The task uid does not exists", body = ResponseError, content_type = "application/json", example = json!(
|
||||
{
|
||||
"message": "Task :taskUid not found.",
|
||||
"code": "task_not_found",
|
||||
"type": "invalid_request",
|
||||
"link": "https://docs.meilisearch.com/errors/#task_not_found"
|
||||
}
|
||||
))
|
||||
)
|
||||
)]
|
||||
async fn cancel_tasks(
|
||||
index_scheduler: GuardedData<ActionPolicy<{ actions::TASKS_CANCEL }>, Data<IndexScheduler>>,
|
||||
params: AwebQueryParameter<TaskDeletionOrCancelationQuery, DeserrQueryParamError>,
|
||||
|
@ -275,6 +385,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 +478,70 @@ 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 +566,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>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue