diff --git a/crates/meilisearch-types/src/star_or.rs b/crates/meilisearch-types/src/star_or.rs index cd26a1fb0..6af833ed8 100644 --- a/crates/meilisearch-types/src/star_or.rs +++ b/crates/meilisearch-types/src/star_or.rs @@ -6,6 +6,7 @@ use std::str::FromStr; use deserr::{DeserializeError, Deserr, MergeWithError, ValueKind}; use serde::de::Visitor; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use utoipa::{IntoParams, PartialSchema, ToSchema}; use crate::deserr::query_params::FromQueryParameter; @@ -229,7 +230,7 @@ pub enum OptionStarOrList { List(Vec), } -impl OptionStarOrList { +impl OptionStarOrList { pub fn is_some(&self) -> bool { match self { Self::None => false, diff --git a/crates/meilisearch/src/routes/batches.rs b/crates/meilisearch/src/routes/batches.rs index 7cdca0bf7..36bf31605 100644 --- a/crates/meilisearch/src/routes/batches.rs +++ b/crates/meilisearch/src/routes/batches.rs @@ -74,7 +74,7 @@ async fn get_batches( let next = if results.len() == limit as usize { results.pop().map(|t| t.uid) } else { None }; let from = results.first().map(|t| t.uid); - let tasks = AllBatches { results, limit: limit.saturating_sub(1) as u32, total, from, next }; + let tasks = AllBatches { results, limit: limit.saturating_sub(1), total, from, next }; Ok(HttpResponse::Ok().json(tasks)) } diff --git a/crates/meilisearch/src/routes/indexes/documents.rs b/crates/meilisearch/src/routes/indexes/documents.rs index 205b71420..ca73ac8b3 100644 --- a/crates/meilisearch/src/routes/indexes/documents.rs +++ b/crates/meilisearch/src/routes/indexes/documents.rs @@ -74,7 +74,7 @@ pub struct DocumentParam { #[derive(OpenApi)] #[openapi( - paths(get_documents, replace_documents, update_documents, clear_all_documents, delete_documents_batch), + paths(get_document, get_documents, delete_document, replace_documents, update_documents, clear_all_documents, delete_documents_batch, delete_documents_by_filter, edit_documents_by_function, documents_by_query_post), tags( ( name = "Documents", @@ -107,12 +107,14 @@ pub fn configure(cfg: &mut web::ServiceConfig) { ); } -#[derive(Debug, Deserr)] +#[derive(Debug, Deserr, IntoParams)] #[deserr(error = DeserrQueryParamError, rename_all = camelCase, deny_unknown_fields)] pub struct GetDocument { #[deserr(default, error = DeserrQueryParamError)] + #[param(value_type = Option>)] fields: OptionStarOrList, #[deserr(default, error = DeserrQueryParamError)] + #[param(value_type = Option)] retrieve_vectors: Param, } @@ -188,6 +190,56 @@ impl Aggregate for DocumentsFetchAggregator { } } + +/// Get one document +/// +/// Get one document from its primary key. +#[utoipa::path( + get, + path = "/{indexUid}/documents/{documentId}", + tags = ["Indexes", "Documents"], + security(("Bearer" = ["documents.get", "documents.*", "*"])), + params( + ("indexUid" = String, Path, example = "movies", description = "Index Unique Identifier", nullable = false), + ("documentId" = String, Path, example = "85087", description = "The document identifier", nullable = false), + GetDocument, + ), + responses( + (status = 200, description = "The documents are returned", body = serde_json::Value, content_type = "application/json", example = json!( + { + "id": 25684, + "title": "American Ninja 5", + "poster": "https://image.tmdb.org/t/p/w1280/iuAQVI4mvjI83wnirpD8GVNRVuY.jpg", + "overview": "When a scientists daughter is kidnapped, American Ninja, attempts to find her, but this time he teams up with a youngster he has trained in the ways of the ninja.", + "release_date": 725846400 + } + )), + (status = 404, description = "Index not found", body = ResponseError, content_type = "application/json", example = json!( + { + "message": "Index `movies` not found.", + "code": "index_not_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#index_not_found" + } + )), + (status = 404, description = "Document not found", body = ResponseError, content_type = "application/json", example = json!( + { + "message": "Document `a` not found.", + "code": "document_not_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#document_not_found" + } + )), + (status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!( + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" + } + )), + ) +)] pub async fn get_document( index_scheduler: GuardedData, Data>, document_param: web::Path, @@ -251,6 +303,39 @@ impl Aggregate for DocumentsDeletionAggregator { } } + +/// Delete a document +/// +/// Delete a single document by id. +#[utoipa::path( + delete, + path = "/{indexUid}/documents/{documentsId}", + tags = ["Indexes", "Documents"], + security(("Bearer" = ["documents.delete", "documents.*", "*"])), + params( + ("indexUid" = String, Path, example = "movies", description = "Index Unique Identifier", nullable = false), + ("documentsId" = String, Path, example = "movies", description = "Document Identifier", nullable = false), + ), + responses( + (status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!( + { + "taskUid": 147, + "indexUid": null, + "status": "enqueued", + "type": "documentAdditionOrUpdate", + "enqueuedAt": "2024-08-08T17:05:55.791772Z" + } + )), + (status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!( + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" + } + )), + ) +)] pub async fn delete_document( index_scheduler: GuardedData, Data>, path: web::Path, @@ -321,6 +406,58 @@ pub struct BrowseQuery { filter: Option, } +/// Get documents with POST +/// +/// Get a set of documents. +#[utoipa::path( + post, + path = "/{indexUid}/documents/fetch", + tags = ["Indexes", "Documents"], + security(("Bearer" = ["documents.delete", "documents.*", "*"])), + params( + ("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false), + BrowseQuery, + ), + responses( + (status = 200, description = "Task successfully enqueued", body = PaginationView, content_type = "application/json", example = json!( + { + "results":[ + { + "title":"The Travels of Ibn Battuta", + "genres":[ + "Travel", + "Adventure" + ], + "language":"English", + "rating":4.5 + }, + { + "title":"Pride and Prejudice", + "genres":[ + "Classics", + "Fiction", + "Romance", + "Literature" + ], + "language":"English", + "rating":4 + }, + ], + "offset":0, + "limit":2, + "total":5 + } + )), + (status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!( + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" + } + )), + ) +)] pub async fn documents_by_query_post( index_scheduler: GuardedData, Data>, index_uid: web::Path, @@ -356,12 +493,10 @@ pub async fn documents_by_query_post( 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 - (status = 200, description = "The documents are returned", body = serde_json::Value, content_type = "application/json", example = json!( + (status = 200, description = "The documents are returned", body = PaginationView, content_type = "application/json", example = json!( { "results": [ { @@ -922,8 +1057,8 @@ async fn copy_body_to_file( security(("Bearer" = ["documents.delete", "documents.*", "*"])), params( ("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false), + // TODO: how to task an array of strings in parameter ), - // TODO: how to return an array of strings responses( (status = 200, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!( { @@ -983,13 +1118,45 @@ pub async fn delete_documents_batch( Ok(HttpResponse::Accepted().json(task)) } -#[derive(Debug, Deserr)] +#[derive(Debug, Deserr, IntoParams)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct DocumentDeletionByFilter { #[deserr(error = DeserrJsonError, missing_field_error = DeserrJsonError::missing_document_filter)] filter: Value, } +/// Delete documents by filter +/// +/// Delete a set of documents based on a filter. +#[utoipa::path( + post, + path = "/{indexUid}/documents/delete", + tags = ["Indexes", "Documents"], + security(("Bearer" = ["documents.delete", "documents.*", "*"])), + params( + ("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false), + DocumentDeletionByFilter, + ), + responses( + (status = 202, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!( + { + "taskUid": 147, + "indexUid": null, + "status": "enqueued", + "type": "documentDeletion", + "enqueuedAt": "2024-08-08T17:05:55.791772Z" + } + )), + (status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!( + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" + } + )), + ) +)] pub async fn delete_documents_by_filter( index_scheduler: GuardedData, Data>, index_uid: web::Path, @@ -1030,7 +1197,7 @@ pub async fn delete_documents_by_filter( Ok(HttpResponse::Accepted().json(task)) } -#[derive(Debug, Deserr)] +#[derive(Debug, Deserr, IntoParams)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct DocumentEditionByFunction { #[deserr(default, error = DeserrJsonError)] @@ -1069,6 +1236,38 @@ impl Aggregate for EditDocumentsByFunctionAggregator { } } +/// Edit documents by function. +/// +/// Use a [RHAI function](https://rhai.rs/book/engine/hello-world.html) to edit one or more documents directly in Meilisearch. +#[utoipa::path( + post, + path = "/{indexUid}/documents/edit", + tags = ["Indexes", "Documents"], + security(("Bearer" = ["documents.*", "*"])), + params( + ("indexUid", example = "movies", description = "Index Unique Identifier", nullable = false), + DocumentEditionByFunction, + ), + responses( + (status = 202, description = "Task successfully enqueued", body = SummarizedTaskView, content_type = "application/json", example = json!( + { + "taskUid": 147, + "indexUid": null, + "status": "enqueued", + "type": "documentDeletion", + "enqueuedAt": "2024-08-08T17:05:55.791772Z" + } + )), + (status = 401, description = "The authorization header is missing", body = ResponseError, content_type = "application/json", example = json!( + { + "message": "The Authorization header is missing. It must use the bearer authorization method.", + "code": "missing_authorization_header", + "type": "auth", + "link": "https://docs.meilisearch.com/errors#missing_authorization_header" + } + )), + ) +)] pub async fn edit_documents_by_function( index_scheduler: GuardedData, Data>, index_uid: web::Path, diff --git a/crates/meilisearch/src/routes/mod.rs b/crates/meilisearch/src/routes/mod.rs index 5ee8498d2..ddd6b6139 100644 --- a/crates/meilisearch/src/routes/mod.rs +++ b/crates/meilisearch/src/routes/mod.rs @@ -66,7 +66,7 @@ pub mod tasks; (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, Settings, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings, SummarizedTaskView, Kind)) + components(schemas(PaginationView, BrowseQuery, UpdateIndexRequest, IndexUid, IndexCreateRequest, KeyView, Action, CreateApiKey, UpdateStderrLogs, LogMode, GetLogs, IndexStats, Stats, HealthStatus, HealthResponse, VersionResponse, Code, ErrorType, AllTasks, TaskView, Status, DetailsView, ResponseError, Settings, Settings, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings, SummarizedTaskView, Kind)) )] pub struct MeilisearchApi; @@ -177,7 +177,7 @@ pub struct Pagination { pub limit: usize, } -#[derive(Debug, Clone, Serialize)] +#[derive(Debug, Clone, Serialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct PaginationView { pub results: Vec, diff --git a/crates/meilisearch/src/routes/tasks.rs b/crates/meilisearch/src/routes/tasks.rs index c01e0139c..2f3871c1a 100644 --- a/crates/meilisearch/src/routes/tasks.rs +++ b/crates/meilisearch/src/routes/tasks.rs @@ -67,7 +67,7 @@ pub struct TasksFilterQuery { /// Permits to filter tasks by their batch uid. By default, when the `batchUids` query parameter is not set, all task uids are returned. It's possible to specify several batch uids by separating them with the `,` character. #[deserr(default, error = DeserrQueryParamError)] - #[param(required = false, value_type = Option, example = 12421)] + #[param(required = false, value_type = Option, example = 12421)] pub batch_uids: OptionStarOrList, /// 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.