diff --git a/meilisearch/src/routes/indexes/documents.rs b/meilisearch/src/routes/indexes/documents.rs index f222f5b69..5c1e385fd 100644 --- a/meilisearch/src/routes/indexes/documents.rs +++ b/meilisearch/src/routes/indexes/documents.rs @@ -17,6 +17,7 @@ use meilisearch_types::error::{Code, ResponseError}; use meilisearch_types::heed::RoTxn; use meilisearch_types::index_uid::IndexUid; use meilisearch_types::milli::update::IndexDocumentsMethod; +use meilisearch_types::milli::InternalError; use meilisearch_types::star_or::OptionStarOrList; use meilisearch_types::tasks::KindWithContent; use meilisearch_types::{milli, Document, Index}; @@ -373,22 +374,96 @@ async fn document_addition( Ok(task.into()) } +#[derive(Debug, Deserialize)] +#[serde(untagged)] +pub enum DocumentDeletionQuery { + Ids(Vec), + Object { filter: Option }, +} + +/// Parses a Json encoded document id and validate it, returning a user error when it is one. +/// FIXME: stolen from milli +fn validate_document_id_value(document_id: Value) -> String { + match document_id { + Value::String(string) => match validate_document_id(&string) { + Some(s) if s.len() == string.len() => string, + Some(s) => s.to_string(), + None => panic!(), + }, + Value::Number(number) if number.is_i64() => number.to_string(), + _content => panic!(), + } +} + +/// FIXME: stolen from milli +fn validate_document_id(document_id: &str) -> Option<&str> { + if !document_id.is_empty() + && document_id.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_')) + { + Some(document_id) + } else { + None + } +} + pub async fn delete_documents( index_scheduler: GuardedData, Data>, index_uid: web::Path, - body: web::Json>, + body: web::Json, req: HttpRequest, analytics: web::Data, ) -> Result { debug!("called with params: {:?}", body); let index_uid = IndexUid::try_from(index_uid.into_inner())?; + let index_uid = index_uid.into_inner(); analytics.delete_documents(DocumentDeletionKind::PerBatch, &req); - let ids = body - .iter() - .map(|v| v.as_str().map(String::from).unwrap_or_else(|| v.to_string())) - .collect(); + let ids = match body.into_inner() { + DocumentDeletionQuery::Ids(body) => body + .iter() + .map(|v| v.as_str().map(String::from).unwrap_or_else(|| v.to_string())) + .collect(), + DocumentDeletionQuery::Object { filter } => { + debug!("filter: {:?}", filter); + + // FIXME: spawn_blocking + if let Some(ref filter) = filter { + if let Some(facets) = crate::search::parse_filter(filter)? { + debug!("facets: {:?}", facets); + + let index = index_scheduler.index(&index_uid)?; + let rtxn = index.read_txn()?; + let filtered_candidates = facets.evaluate(&rtxn, &index)?; + debug!("filtered_candidates.len(): {:?}", filtered_candidates.len()); + + // FIXME: unwraps + let primary_key = index.primary_key(&rtxn)?.unwrap(); + let primary_key = index.fields_ids_map(&rtxn)?.id(primary_key).unwrap(); + + let documents = index.documents(&rtxn, filtered_candidates.into_iter())?; + debug!("documents.len(): {:?}", documents.len()); + let documents: Vec = documents + .into_iter() + .map(|(_, document)| { + let value = document.get(primary_key).unwrap(); + let value: Value = serde_json::from_slice(value) + .map_err(InternalError::SerdeJson) + .unwrap(); + + validate_document_id_value(value) + }) + .collect(); + debug!("documents: {:?}", documents); + documents + } else { + vec![] + } + } else { + vec![] + } + } + }; let task = KindWithContent::DocumentDeletion { index_uid: index_uid.to_string(), documents_ids: ids }; diff --git a/meilisearch/src/search.rs b/meilisearch/src/search.rs index 7e4a7da6a..81c5972b8 100644 --- a/meilisearch/src/search.rs +++ b/meilisearch/src/search.rs @@ -745,7 +745,7 @@ fn format_value>( } } -fn parse_filter(facets: &Value) -> Result, MeilisearchHttpError> { +pub fn parse_filter(facets: &Value) -> Result, MeilisearchHttpError> { match facets { Value::String(expr) => { let condition = Filter::from_str(expr)?;