diff --git a/meilisearch-core/src/error.rs b/meilisearch-core/src/error.rs index 370147d59..110d6ab76 100644 --- a/meilisearch-core/src/error.rs +++ b/meilisearch-core/src/error.rs @@ -1,10 +1,13 @@ use crate::serde::{DeserializerError, SerializerError}; use serde_json::Error as SerdeJsonError; +use pest::error::Error as PestError; +use crate::filters::Rule; use std::{error, fmt, io}; -pub use heed::Error as HeedError; -pub use fst::Error as FstError; pub use bincode::Error as BincodeError; +pub use fst::Error as FstError; +pub use heed::Error as HeedError; +pub use pest::error as pest_error; pub type MResult = Result; @@ -25,6 +28,7 @@ pub enum Error { Serializer(SerializerError), Deserializer(DeserializerError), UnsupportedOperation(UnsupportedOperation), + FilterParseError(PestError) } impl From for Error { @@ -42,11 +46,11 @@ impl From> for Error { Rule::not => "NOT", Rule::string => "string", Rule::word => "word", - Rule::greater => "field>value", - Rule::less => "field "field:value", - Rule::leq => "field<=value", - Rule::geq => "field>=value", + Rule::greater => "field > value", + Rule::less => "field < value", + Rule::eq => "field = value", + Rule::leq => "field <= value", + Rule::geq => "field >= value", Rule::key => "key", _ => "other", }; @@ -122,6 +126,7 @@ impl fmt::Display for Error { Serializer(e) => write!(f, "serializer error; {}", e), Deserializer(e) => write!(f, "deserializer error; {}", e), UnsupportedOperation(op) => write!(f, "unsupported operation; {}", op), + FilterParseError(e) => write!(f, "error parsing filter; {}", e), } } } diff --git a/meilisearch-core/src/lib.rs b/meilisearch-core/src/lib.rs index f3e4b0c27..6ba37259b 100644 --- a/meilisearch-core/src/lib.rs +++ b/meilisearch-core/src/lib.rs @@ -1,12 +1,15 @@ #[cfg(test)] #[macro_use] extern crate assert_matches; +#[macro_use] +extern crate pest_derive; mod automaton; mod bucket_sort; mod database; mod distinct_map; mod error; +mod filters; mod levenshtein; mod number; mod query_builder; @@ -23,7 +26,8 @@ pub mod serde; pub mod store; pub use self::database::{BoxUpdateFn, Database, MainT, UpdateT}; -pub use self::error::{Error, HeedError, FstError, MResult}; +pub use self::error::{Error, HeedError, FstError, MResult, pest_error}; +pub use self::filters::Filter; pub use self::number::{Number, ParseNumberError}; pub use self::ranked_map::RankedMap; pub use self::raw_document::RawDocument; diff --git a/meilisearch-http/src/error.rs b/meilisearch-http/src/error.rs index 9c1a91a4e..a4120b2ee 100644 --- a/meilisearch-http/src/error.rs +++ b/meilisearch-http/src/error.rs @@ -19,6 +19,7 @@ pub enum ResponseError { IndexNotFound(String), DocumentNotFound(String), MissingHeader(String), + FilterParsing(String), BadParameter(String, String), OpenIndex(String), CreateIndex(String), @@ -73,11 +74,15 @@ impl IntoResponse for ResponseError { match self { ResponseError::Internal(err) => { error!("internal server error: {}", err); - error( - String::from("Internal server error"), + error("Internal server error".to_string(), StatusCode::INTERNAL_SERVER_ERROR, ) } + ResponseError::FilterParsing(err) => { + warn!("error paring filter: {}", err); + error(format!("parsing error: {}", err), + StatusCode::BAD_REQUEST) + } ResponseError::BadRequest(err) => { warn!("bad request: {}", err); error(err, StatusCode::BAD_REQUEST) @@ -159,7 +164,10 @@ impl From for ResponseError { impl From for ResponseError { fn from(err: SearchError) -> ResponseError { - ResponseError::internal(err) + match err { + SearchError::FilterParsing(s) => ResponseError::FilterParsing(s), + _ => ResponseError::internal(err), + } } } diff --git a/meilisearch-http/src/helpers/meilisearch.rs b/meilisearch-http/src/helpers/meilisearch.rs index 8408d4904..6cb1f97a3 100644 --- a/meilisearch-http/src/helpers/meilisearch.rs +++ b/meilisearch-http/src/helpers/meilisearch.rs @@ -8,6 +8,7 @@ use std::time::{Duration, Instant}; use indexmap::IndexMap; use log::error; +use meilisearch_core::Filter; use meilisearch_core::criterion::*; use meilisearch_core::settings::RankingRule; use meilisearch_core::{Highlight, Index, MainT, RankedMap}; @@ -23,6 +24,7 @@ pub enum Error { RetrieveDocument(u64, String), DocumentNotFound(u64), CropFieldWrongType(String), + FilterParsing(String), AttributeNotFoundOnDocument(String), AttributeNotFoundOnSchema(String), MissingFilterValue, @@ -56,13 +58,26 @@ impl fmt::Display for Error { f.write_str("a filter is specifying an unknown schema attribute") } Internal(err) => write!(f, "internal error; {}", err), + FilterParsing(err) => write!(f, "filter parsing error: {}", err), } } } impl From for Error { fn from(error: meilisearch_core::Error) -> Self { - Error::Internal(error.to_string()) + use meilisearch_core::pest_error::LineColLocation::*; + match error { + meilisearch_core::Error::FilterParseError(e) => { + let (line, column) = match e.line_col { + Span((line, _), (column, _)) => (line, column), + Pos((line, column)) => (line, column), + }; + let message = format!("parsing error on line {} at column {}: {}", line, column, e.variant.message()); + + Error::FilterParsing(message) + }, + _ => Error::Internal(error.to_string()), + } } } @@ -171,39 +186,20 @@ impl<'a> SearchBuilder<'a> { None => self.index.query_builder(), }; - if let Some(filters) = &self.filters { - let mut split = filters.split(':'); - match (split.next(), split.next()) { - (Some(_), None) | (Some(_), Some("")) => return Err(Error::MissingFilterValue), - (Some(attr), Some(value)) => { - let ref_reader = reader; - let ref_index = &self.index; - let value = value.trim().to_lowercase(); - - let attr = match schema.id(attr) { - Some(attr) => attr, - None => return Err(Error::UnknownFilteredAttribute), - }; - - query_builder.with_filter(move |id| { - let attr = attr; - let index = ref_index; - let reader = ref_reader; - - match index.document_attribute::(reader, id, attr) { - Ok(Some(Value::String(s))) => s.to_lowercase() == value, - Ok(Some(Value::Bool(b))) => { - (value == "true" && b) || (value == "false" && !b) - } - Ok(Some(Value::Array(a))) => { - a.into_iter().any(|s| s.as_str() == Some(&value)) - } - _ => false, - } - }); + if let Some(filter_expression) = &self.filters { + let filter = Filter::parse(filter_expression, &schema)?; + query_builder.with_filter(move |id| { + let index = &self.index; + let reader = &reader; + let filter = &filter; + match filter.test(reader, index, id) { + Ok(res) => res, + Err(e) => { + log::warn!("unexpected error during filtering: {}", e); + false + } } - (_, _) => (), - } + }); } query_builder.with_fetch_timeout(self.timeout);