MeiliSearch/crates/milli/src/attribute_patterns.rs
2025-03-10 13:46:33 +01:00

153 lines
5.8 KiB
Rust

use deserr::Deserr;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use crate::is_faceted_by;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[repr(transparent)]
#[serde(transparent)]
pub struct AttributePatterns {
#[schema(example = json!(["title", "overview_*", "release_date"]))]
pub patterns: Vec<String>,
}
impl<E: deserr::DeserializeError> Deserr<E> for AttributePatterns {
fn deserialize_from_value<V: deserr::IntoValue>(
value: deserr::Value<V>,
location: deserr::ValuePointerRef,
) -> Result<Self, E> {
Vec::<String>::deserialize_from_value(value, location).map(|patterns| Self { patterns })
}
}
impl From<Vec<String>> for AttributePatterns {
fn from(patterns: Vec<String>) -> Self {
Self { patterns }
}
}
impl AttributePatterns {
/// Match a string against the attribute patterns using the match_pattern function.
pub fn match_str(&self, str: &str) -> PatternMatch {
let mut pattern_match = PatternMatch::NoMatch;
for pattern in &self.patterns {
match match_pattern(pattern, str) {
PatternMatch::Match => return PatternMatch::Match,
PatternMatch::Parent => pattern_match = PatternMatch::Parent,
PatternMatch::NoMatch => {}
}
}
pattern_match
}
}
/// Match a string against a pattern.
///
/// The pattern can be a wildcard, a prefix, a suffix or an exact match.
///
/// # Arguments
///
/// * `pattern` - The pattern to match against.
/// * `str` - The string to match against the pattern.
fn match_pattern(pattern: &str, str: &str) -> PatternMatch {
// If the pattern is a wildcard, return Match
if pattern == "*" {
return PatternMatch::Match;
} else if pattern.starts_with('*') && pattern.ends_with('*') {
// If the pattern starts and ends with a wildcard, return Match if the string contains the pattern without the wildcards
if str.contains(&pattern[1..pattern.len() - 1]) {
return PatternMatch::Match;
}
} else if let Some(pattern) = pattern.strip_prefix('*') {
// If the pattern starts with a wildcard, return Match if the string ends with the pattern without the wildcard
if str.ends_with(pattern) {
return PatternMatch::Match;
}
} else if let Some(pattern) = pattern.strip_suffix('*') {
// If the pattern ends with a wildcard, return Match if the string starts with the pattern without the wildcard
if str.starts_with(pattern) {
return PatternMatch::Match;
}
} else if pattern == str {
// If the pattern is exactly the string, return Match
return PatternMatch::Match;
}
// If the field is a parent field of the pattern, return Parent
if is_faceted_by(pattern, str) {
PatternMatch::Parent
} else {
PatternMatch::NoMatch
}
}
/// Match a field against a pattern using the legacy behavior.
///
/// A field matches a pattern if it is a parent of the pattern or if it is the pattern itself.
/// This behavior is used to match the sortable attributes, the searchable attributes and the filterable attributes rules `Field`.
///
/// # Arguments
///
/// * `pattern` - The pattern to match against.
/// * `field` - The field to match against the pattern.
pub fn match_field_legacy(pattern: &str, field: &str) -> PatternMatch {
if is_faceted_by(field, pattern) {
// If the field matches the pattern or is a nested field of the pattern, return Match (legacy behavior)
PatternMatch::Match
} else if is_faceted_by(pattern, field) {
// If the field is a parent field of the pattern, return Parent
PatternMatch::Parent
} else {
// If the field does not match the pattern and is not a parent of a nested field that matches the pattern, return NoMatch
PatternMatch::NoMatch
}
}
/// Match a field against a distinct field.
pub fn match_distinct_field(distinct_field: Option<&str>, field: &str) -> PatternMatch {
if let Some(distinct_field) = distinct_field {
if field == distinct_field {
// If the field matches exactly the distinct field, return Match
return PatternMatch::Match;
} else if is_faceted_by(distinct_field, field) {
// If the field is a parent field of the distinct field, return Parent
return PatternMatch::Parent;
}
}
// If the field does not match the distinct field and is not a parent of a nested field that matches the distinct field, return NoMatch
PatternMatch::NoMatch
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PatternMatch {
/// The field is a parent of a nested field that matches the pattern
/// For example, the field is `toto`, and the pattern is `toto.titi`
Parent,
/// The field matches the pattern
Match,
/// The field does not match the pattern
NoMatch,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_match_pattern() {
assert_eq!(match_pattern("*", "test"), PatternMatch::Match);
assert_eq!(match_pattern("test*", "test"), PatternMatch::Match);
assert_eq!(match_pattern("test*", "testa"), PatternMatch::Match);
assert_eq!(match_pattern("*test", "test"), PatternMatch::Match);
assert_eq!(match_pattern("*test", "atest"), PatternMatch::Match);
assert_eq!(match_pattern("*test*", "test"), PatternMatch::Match);
assert_eq!(match_pattern("*test*", "atesta"), PatternMatch::Match);
assert_eq!(match_pattern("*test*", "atest"), PatternMatch::Match);
assert_eq!(match_pattern("*test*", "testa"), PatternMatch::Match);
assert_eq!(match_pattern("test*test", "test"), PatternMatch::NoMatch);
assert_eq!(match_pattern("*test", "testa"), PatternMatch::NoMatch);
assert_eq!(match_pattern("test*", "atest"), PatternMatch::NoMatch);
}
}