diff --git a/crates/filter-parser/src/condition.rs b/crates/filter-parser/src/condition.rs index e4795d048..bdc5038e8 100644 --- a/crates/filter-parser/src/condition.rs +++ b/crates/filter-parser/src/condition.rs @@ -159,10 +159,8 @@ fn parse_vectors(input: Span) -> IResult<(Token, Option, VectorFilter<'_> if let Ok((input, point)) = tag::<_, _, ()>(".")(input) { let opt_value = parse_vector_value(input).ok().map(|(_, v)| v); - let value = opt_value - .as_ref() - .map(|v| v.original_span().to_string()) - .unwrap_or_else(|| point.to_string()); + let value = + opt_value.as_ref().map(|v| v.value().to_owned()).unwrap_or_else(|| point.to_string()); let context = opt_value.map(|v| v.original_span()).unwrap_or(point); return Err(Error::failure_from_kind(context, ErrorKind::VectorFilterUnknownSuffix(value))); } diff --git a/crates/meilisearch/tests/search/filters.rs b/crates/meilisearch/tests/search/filters.rs index 8e3ee9249..9cd6575ac 100644 --- a/crates/meilisearch/tests/search/filters.rs +++ b/crates/meilisearch/tests/search/filters.rs @@ -952,13 +952,13 @@ async fn vector_filter_non_existant_fragment() { let (value, _code) = index .search_post(json!({ - "filter": "_vectors.rest.fragments.other EXISTS", + "filter": "_vectors.rest.fragments.withBred EXISTS", "attributesToRetrieve": ["name"] })) .await; snapshot!(value, @r#" { - "message": "Index `[uuid]`: The fragment `other` does not exist on embedder `rest`. Available fragments on this embedder are: `basic`, `withBreed`.\n25:30 _vectors.rest.fragments.other EXISTS", + "message": "Index `[uuid]`: The fragment `withBred` does not exist on embedder `rest`. Available fragments on this embedder are: `basic`, `withBreed`. Did you mean `withBreed`?\n25:33 _vectors.rest.fragments.withBred EXISTS", "code": "invalid_search_filter", "type": "invalid_request", "link": "https://docs.meilisearch.com/errors#invalid_search_filter" diff --git a/crates/milli/src/error.rs b/crates/milli/src/error.rs index 9ad9d0511..76ad3fda0 100644 --- a/crates/milli/src/error.rs +++ b/crates/milli/src/error.rs @@ -639,3 +639,29 @@ fn conditionally_lookup_for_error_message() { assert_eq!(err.to_string(), format!("{} {}", prefix, suffix)); } } + +pub struct DidYouMean<'a>(Option<&'a str>); + +impl<'a> DidYouMean<'a> { + pub fn new(key: &str, keys: &'a [String]) -> DidYouMean<'a> { + let typos = levenshtein_automata::LevenshteinAutomatonBuilder::new(2, true).build_dfa(key); + for key in keys.iter() { + match typos.eval(key) { + levenshtein_automata::Distance::Exact(_) => { + return DidYouMean(Some(key)); + } + levenshtein_automata::Distance::AtLeast(_) => continue, + } + } + DidYouMean(None) + } +} + +impl std::fmt::Display for DidYouMean<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(suggestion) = self.0 { + write!(f, " Did you mean `{suggestion}`?")?; + } + Ok(()) + } +} diff --git a/crates/milli/src/search/facet/filter_vector.rs b/crates/milli/src/search/facet/filter_vector.rs index 625bd5dde..1ef4b8e3d 100644 --- a/crates/milli/src/search/facet/filter_vector.rs +++ b/crates/milli/src/search/facet/filter_vector.rs @@ -1,7 +1,7 @@ use filter_parser::{Token, VectorFilter}; use roaring::{MultiOps, RoaringBitmap}; -use crate::error::Error; +use crate::error::{DidYouMean, Error}; use crate::vector::db::IndexEmbeddingConfig; use crate::vector::{ArroyStats, ArroyWrapper}; use crate::Index; @@ -14,7 +14,8 @@ pub enum VectorFilterError<'a> { } else { let mut available = available.clone(); available.sort_unstable(); - format!("Available embedders are: {}.", available.iter().map(|e| format!("`{e}`")).collect::>().join(", ")) + let did_you_mean = DidYouMean::new(embedder.value(), &available); + format!("Available embedders are: {}.{did_you_mean}", available.iter().map(|e| format!("`{e}`")).collect::>().join(", ")) } })] EmbedderDoesNotExist { embedder: &'a Token<'a>, available: Vec }, @@ -25,7 +26,8 @@ pub enum VectorFilterError<'a> { } else { let mut available = available.clone(); available.sort_unstable(); - format!("Available fragments on this embedder are: {}.", available.iter().map(|f| format!("`{f}`")).collect::>().join(", ")) + let did_you_mean = DidYouMean::new(fragment.value(), &available); + format!("Available fragments on this embedder are: {}.{did_you_mean}", available.iter().map(|f| format!("`{f}`")).collect::>().join(", ")) } })] FragmentDoesNotExist {