use std::collections::HashMap; use std::fmt::Write; use itertools::Itertools as _; use meilisearch_types::error::{Code, ResponseError}; use meilisearch_types::milli::{AscDesc, Criterion, Member, TermsMatchingStrategy}; pub struct RankingRules { canonical_criteria: Vec, canonical_sort: Option>, canonicalization_actions: Vec, source_criteria: Vec, source_sort: Option>, } pub enum CanonicalizationAction { PrependedWords { prepended_index: RankingRuleSource, }, RemovedDuplicate { earlier_occurrence: RankingRuleSource, removed_occurrence: RankingRuleSource, }, RemovedWords { reason: RemoveWords, removed_occurrence: RankingRuleSource, }, RemovedPlaceholder { removed_occurrence: RankingRuleSource, }, TruncatedVector { vector_rule: RankingRuleSource, truncated_from: RankingRuleSource, }, RemovedVector { vector_rule: RankingRuleSource, removed_occurrence: RankingRuleSource, }, RemovedSort { removed_occurrence: RankingRuleSource, }, } pub enum RemoveWords { WasPrepended, MatchingStrategyAll, } impl std::fmt::Display for RemoveWords { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let reason = match self { RemoveWords::WasPrepended => "it was previously prepended", RemoveWords::MatchingStrategyAll => "`query.matchingWords` is set to `all`", }; f.write_str(reason) } } pub enum CanonicalizationKind { Placeholder, Keyword, Vector, } pub struct CompatibilityError { previous: RankingRule, current: RankingRule, } impl CompatibilityError { pub(crate) fn to_response_error( &self, ranking_rules: &RankingRules, previous_ranking_rules: &RankingRules, query_index: usize, previous_query_index: usize, index_uid: &str, previous_index_uid: &str, ) -> meilisearch_types::error::ResponseError { let rule = self.current.as_string( &ranking_rules.canonical_criteria, &ranking_rules.canonical_sort, query_index, index_uid, ); let previous_rule = self.previous.as_string( &previous_ranking_rules.canonical_criteria, &previous_ranking_rules.canonical_sort, previous_query_index, previous_index_uid, ); let canonicalization_actions = ranking_rules.canonicalization_notes(); let previous_canonicalization_actions = previous_ranking_rules.canonicalization_notes(); let mut msg = String::new(); let reason = self.reason(); let _ = writeln!( &mut msg, "The results of queries #{previous_query_index} and #{query_index} are incompatible: " ); let _ = writeln!(&mut msg, " 1. {previous_rule}"); let _ = writeln!(&mut msg, " 2. {rule}"); let _ = writeln!(&mut msg, " - {reason}"); if !previous_canonicalization_actions.is_empty() { let _ = write!(&mut msg, " - note: The ranking rules of query #{previous_query_index} were modified during canonicalization:\n{previous_canonicalization_actions}"); } if !canonicalization_actions.is_empty() { let _ = write!(&mut msg, " - note: The ranking rules of query #{query_index} were modified during canonicalization:\n{canonicalization_actions}"); } ResponseError::from_msg(msg, Code::InvalidMultiSearchQueryRankingRules) } pub fn reason(&self) -> &'static str { match (self.previous.kind, self.current.kind) { (RankingRuleKind::Relevancy, RankingRuleKind::AscendingSort) | (RankingRuleKind::Relevancy, RankingRuleKind::DescendingSort) | (RankingRuleKind::AscendingSort, RankingRuleKind::Relevancy) | (RankingRuleKind::DescendingSort, RankingRuleKind::Relevancy) => { "cannot compare a relevancy rule with a sort rule" } (RankingRuleKind::Relevancy, RankingRuleKind::AscendingGeoSort) | (RankingRuleKind::Relevancy, RankingRuleKind::DescendingGeoSort) | (RankingRuleKind::AscendingGeoSort, RankingRuleKind::Relevancy) | (RankingRuleKind::DescendingGeoSort, RankingRuleKind::Relevancy) => { "cannot compare a relevancy rule with a geosort rule" } (RankingRuleKind::AscendingSort, RankingRuleKind::DescendingSort) | (RankingRuleKind::DescendingSort, RankingRuleKind::AscendingSort) => { "cannot compare two sort rules in opposite directions" } (RankingRuleKind::AscendingSort, RankingRuleKind::AscendingGeoSort) | (RankingRuleKind::AscendingSort, RankingRuleKind::DescendingGeoSort) | (RankingRuleKind::DescendingSort, RankingRuleKind::AscendingGeoSort) | (RankingRuleKind::DescendingSort, RankingRuleKind::DescendingGeoSort) | (RankingRuleKind::AscendingGeoSort, RankingRuleKind::AscendingSort) | (RankingRuleKind::AscendingGeoSort, RankingRuleKind::DescendingSort) | (RankingRuleKind::DescendingGeoSort, RankingRuleKind::AscendingSort) | (RankingRuleKind::DescendingGeoSort, RankingRuleKind::DescendingSort) => { "cannot compare a sort rule with a geosort rule" } (RankingRuleKind::AscendingGeoSort, RankingRuleKind::DescendingGeoSort) | (RankingRuleKind::DescendingGeoSort, RankingRuleKind::AscendingGeoSort) => { "cannot compare two geosort rules in opposite directions" } (RankingRuleKind::Relevancy, RankingRuleKind::Relevancy) | (RankingRuleKind::AscendingSort, RankingRuleKind::AscendingSort) | (RankingRuleKind::DescendingSort, RankingRuleKind::DescendingSort) | (RankingRuleKind::AscendingGeoSort, RankingRuleKind::AscendingGeoSort) | (RankingRuleKind::DescendingGeoSort, RankingRuleKind::DescendingGeoSort) => { "internal error, comparison should be possible" } } } } impl RankingRules { pub fn new( criteria: Vec, sort: Option>, terms_matching_strategy: TermsMatchingStrategy, canonicalization_kind: CanonicalizationKind, ) -> Self { let (canonical_criteria, canonical_sort, canonicalization_actions) = Self::canonicalize(&criteria, &sort, terms_matching_strategy, canonicalization_kind); Self { canonical_criteria, canonical_sort, canonicalization_actions, source_criteria: criteria, source_sort: sort, } } fn canonicalize( criteria: &[Criterion], sort: &Option>, terms_matching_strategy: TermsMatchingStrategy, canonicalization_kind: CanonicalizationKind, ) -> (Vec, Option>, Vec) { match canonicalization_kind { CanonicalizationKind::Placeholder => Self::canonicalize_placeholder(criteria, sort), CanonicalizationKind::Keyword => { Self::canonicalize_keyword(criteria, sort, terms_matching_strategy) } CanonicalizationKind::Vector => Self::canonicalize_vector(criteria, sort), } } fn canonicalize_placeholder( criteria: &[Criterion], sort_query: &Option>, ) -> (Vec, Option>, Vec) { let mut sort = None; let mut sorted_fields = HashMap::new(); let mut canonicalization_actions = Vec::new(); let mut canonical_criteria = Vec::new(); let mut canonical_sort = None; for (criterion_index, criterion) in criteria.iter().enumerate() { match criterion.clone() { Criterion::Words | Criterion::Typo | Criterion::Proximity | Criterion::Attribute | Criterion::Exactness => { canonicalization_actions.push(CanonicalizationAction::RemovedPlaceholder { removed_occurrence: RankingRuleSource::Criterion(criterion_index), }) } Criterion::Sort => { if let Some(previous_index) = sort { canonicalization_actions.push(CanonicalizationAction::RemovedDuplicate { earlier_occurrence: RankingRuleSource::Criterion(previous_index), removed_occurrence: RankingRuleSource::Criterion(criterion_index), }); } else if let Some(sort_query) = sort_query { sort = Some(criterion_index); canonical_criteria.push(criterion.clone()); canonical_sort = Some(canonicalize_sort( &mut sorted_fields, sort_query.as_slice(), criterion_index, &mut canonicalization_actions, )); } else { canonicalization_actions.push(CanonicalizationAction::RemovedSort { removed_occurrence: RankingRuleSource::Criterion(criterion_index), }) } } Criterion::Asc(s) | Criterion::Desc(s) => match sorted_fields.entry(s) { std::collections::hash_map::Entry::Occupied(entry) => canonicalization_actions .push(CanonicalizationAction::RemovedDuplicate { earlier_occurrence: *entry.get(), removed_occurrence: RankingRuleSource::Criterion(criterion_index), }), std::collections::hash_map::Entry::Vacant(entry) => { entry.insert(RankingRuleSource::Criterion(criterion_index)); canonical_criteria.push(criterion.clone()) } }, } } (canonical_criteria, canonical_sort, canonicalization_actions) } fn canonicalize_vector( criteria: &[Criterion], sort_query: &Option>, ) -> (Vec, Option>, Vec) { let mut sort = None; let mut sorted_fields = HashMap::new(); let mut canonicalization_actions = Vec::new(); let mut canonical_criteria = Vec::new(); let mut canonical_sort = None; let mut vector = None; 'criteria: for (criterion_index, criterion) in criteria.iter().enumerate() { match criterion.clone() { Criterion::Words | Criterion::Typo | Criterion::Proximity | Criterion::Attribute | Criterion::Exactness => match vector { Some(previous_occurrence) => { if sorted_fields.is_empty() { canonicalization_actions.push(CanonicalizationAction::RemovedVector { vector_rule: RankingRuleSource::Criterion(previous_occurrence), removed_occurrence: RankingRuleSource::Criterion(criterion_index), }); } else { canonicalization_actions.push( CanonicalizationAction::TruncatedVector { vector_rule: RankingRuleSource::Criterion(previous_occurrence), truncated_from: RankingRuleSource::Criterion(criterion_index), }, ); break 'criteria; } } None => { canonical_criteria.push(criterion.clone()); vector = Some(criterion_index); } }, Criterion::Sort => { if let Some(previous_index) = sort { canonicalization_actions.push(CanonicalizationAction::RemovedDuplicate { earlier_occurrence: RankingRuleSource::Criterion(previous_index), removed_occurrence: RankingRuleSource::Criterion(criterion_index), }); } else if let Some(sort_query) = sort_query { sort = Some(criterion_index); canonical_criteria.push(criterion.clone()); canonical_sort = Some(canonicalize_sort( &mut sorted_fields, sort_query.as_slice(), criterion_index, &mut canonicalization_actions, )); } else { canonicalization_actions.push(CanonicalizationAction::RemovedSort { removed_occurrence: RankingRuleSource::Criterion(criterion_index), }) } } Criterion::Asc(s) | Criterion::Desc(s) => match sorted_fields.entry(s) { std::collections::hash_map::Entry::Occupied(entry) => canonicalization_actions .push(CanonicalizationAction::RemovedDuplicate { earlier_occurrence: *entry.get(), removed_occurrence: RankingRuleSource::Criterion(criterion_index), }), std::collections::hash_map::Entry::Vacant(entry) => { entry.insert(RankingRuleSource::Criterion(criterion_index)); canonical_criteria.push(criterion.clone()) } }, } } (canonical_criteria, canonical_sort, canonicalization_actions) } fn canonicalize_keyword( criteria: &[Criterion], sort_query: &Option>, terms_matching_strategy: TermsMatchingStrategy, ) -> (Vec, Option>, Vec) { let mut words = None; let mut typo = None; let mut proximity = None; let mut sort = None; let mut attribute = None; let mut exactness = None; let mut sorted_fields = HashMap::new(); let mut canonical_criteria = Vec::new(); let mut canonical_sort = None; let mut canonicalization_actions = Vec::new(); for (criterion_index, criterion) in criteria.iter().enumerate() { let criterion = criterion.clone(); match criterion.clone() { Criterion::Words => { if let TermsMatchingStrategy::All = terms_matching_strategy { canonicalization_actions.push(CanonicalizationAction::RemovedWords { reason: RemoveWords::MatchingStrategyAll, removed_occurrence: RankingRuleSource::Criterion(criterion_index), }); continue; } if let Some(maybe_previous_index) = words { if let Some(previous_index) = maybe_previous_index { canonicalization_actions.push( CanonicalizationAction::RemovedDuplicate { earlier_occurrence: RankingRuleSource::Criterion( previous_index, ), removed_occurrence: RankingRuleSource::Criterion( criterion_index, ), }, ); continue; } canonicalization_actions.push(CanonicalizationAction::RemovedWords { reason: RemoveWords::WasPrepended, removed_occurrence: RankingRuleSource::Criterion(criterion_index), }) } words = Some(Some(criterion_index)); canonical_criteria.push(criterion); } Criterion::Typo => { canonicalize_criterion( criterion, criterion_index, terms_matching_strategy, &mut words, &mut canonicalization_actions, &mut canonical_criteria, &mut typo, ); } Criterion::Proximity => { canonicalize_criterion( criterion, criterion_index, terms_matching_strategy, &mut words, &mut canonicalization_actions, &mut canonical_criteria, &mut proximity, ); } Criterion::Attribute => { canonicalize_criterion( criterion, criterion_index, terms_matching_strategy, &mut words, &mut canonicalization_actions, &mut canonical_criteria, &mut attribute, ); } Criterion::Exactness => { canonicalize_criterion( criterion, criterion_index, terms_matching_strategy, &mut words, &mut canonicalization_actions, &mut canonical_criteria, &mut exactness, ); } Criterion::Sort => { if let Some(previous_index) = sort { canonicalization_actions.push(CanonicalizationAction::RemovedDuplicate { earlier_occurrence: RankingRuleSource::Criterion(previous_index), removed_occurrence: RankingRuleSource::Criterion(criterion_index), }); } else if let Some(sort_query) = sort_query { sort = Some(criterion_index); canonical_criteria.push(criterion); canonical_sort = Some(canonicalize_sort( &mut sorted_fields, sort_query.as_slice(), criterion_index, &mut canonicalization_actions, )); } else { canonicalization_actions.push(CanonicalizationAction::RemovedSort { removed_occurrence: RankingRuleSource::Criterion(criterion_index), }) } } Criterion::Asc(s) | Criterion::Desc(s) => match sorted_fields.entry(s) { std::collections::hash_map::Entry::Occupied(entry) => canonicalization_actions .push(CanonicalizationAction::RemovedDuplicate { earlier_occurrence: *entry.get(), removed_occurrence: RankingRuleSource::Criterion(criterion_index), }), std::collections::hash_map::Entry::Vacant(entry) => { entry.insert(RankingRuleSource::Criterion(criterion_index)); canonical_criteria.push(criterion) } }, } } (canonical_criteria, canonical_sort, canonicalization_actions) } pub fn is_compatible_with(&self, previous: &Self) -> Result<(), CompatibilityError> { for (current, previous) in self.coalesce_iterator().zip(previous.coalesce_iterator()) { if current.kind != previous.kind { return Err(CompatibilityError { current, previous }); } } Ok(()) } pub fn constraint_count(&self) -> usize { self.coalesce_iterator().count() } fn coalesce_iterator(&self) -> impl Iterator + '_ { self.canonical_criteria .iter() .enumerate() .flat_map(|(criterion_index, criterion)| { RankingRule::from_criterion(criterion_index, criterion, &self.canonical_sort) }) .coalesce( |previous @ RankingRule { source: previous_source, kind: previous_kind }, current @ RankingRule { source, kind }| { match (previous_kind, kind) { (RankingRuleKind::Relevancy, RankingRuleKind::Relevancy) => { let merged_source = match (previous_source, source) { ( RankingRuleSource::Criterion(previous), RankingRuleSource::Criterion(current), ) => RankingRuleSource::CoalescedCriteria(previous, current), ( RankingRuleSource::CoalescedCriteria(begin, _end), RankingRuleSource::Criterion(current), ) => RankingRuleSource::CoalescedCriteria(begin, current), (_previous, current) => current, }; Ok(RankingRule { source: merged_source, kind }) } _ => Err((previous, current)), } }, ) } fn canonicalization_notes(&self) -> String { use CanonicalizationAction::*; let mut notes = String::new(); for (index, action) in self.canonicalization_actions.iter().enumerate() { let index = index + 1; let _ = match action { PrependedWords { prepended_index } => writeln!( &mut notes, " {index}. Prepended rule `words` before first relevancy rule `{}` at position {}", prepended_index.rule_name(&self.source_criteria, &self.source_sort), prepended_index.rule_position() ), RemovedDuplicate { earlier_occurrence, removed_occurrence } => writeln!( &mut notes, " {index}. Removed duplicate rule `{}` at position {} as it already appears at position {}", earlier_occurrence.rule_name(&self.source_criteria, &self.source_sort), removed_occurrence.rule_position(), earlier_occurrence.rule_position(), ), RemovedWords { reason, removed_occurrence } => writeln!( &mut notes, " {index}. Removed rule `words` at position {} because {reason}", removed_occurrence.rule_position() ), RemovedPlaceholder { removed_occurrence } => writeln!( &mut notes, " {index}. Removed relevancy rule `{}` at position {} because the query is a placeholder search (`q`: \"\")", removed_occurrence.rule_name(&self.source_criteria, &self.source_sort), removed_occurrence.rule_position() ), TruncatedVector { vector_rule, truncated_from } => writeln!( &mut notes, " {index}. Truncated relevancy rule `{}` at position {} and later rules because the query is a vector search and `vector` was inserted at position {}", truncated_from.rule_name(&self.source_criteria, &self.source_sort), truncated_from.rule_position(), vector_rule.rule_position(), ), RemovedVector { vector_rule, removed_occurrence } => writeln!( &mut notes, " {index}. Removed relevancy rule `{}` at position {} because the query is a vector search and `vector` was already inserted at position {}", removed_occurrence.rule_name(&self.source_criteria, &self.source_sort), removed_occurrence.rule_position(), vector_rule.rule_position(), ), RemovedSort { removed_occurrence } => writeln!( &mut notes, " {index}. Removed rule `sort` at position {} because `query.sort` is empty", removed_occurrence.rule_position() ), }; } notes } } fn canonicalize_sort( sorted_fields: &mut HashMap, sort_query: &[AscDesc], criterion_index: usize, canonicalization_actions: &mut Vec, ) -> Vec { let mut geo_sorted = None; let mut canonical_sort = Vec::new(); for (sort_index, asc_desc) in sort_query.iter().enumerate() { let source = RankingRuleSource::Sort { criterion_index, sort_index }; let asc_desc = asc_desc.clone(); match asc_desc.clone() { AscDesc::Asc(Member::Field(s)) | AscDesc::Desc(Member::Field(s)) => { match sorted_fields.entry(s) { std::collections::hash_map::Entry::Occupied(entry) => canonicalization_actions .push(CanonicalizationAction::RemovedDuplicate { earlier_occurrence: *entry.get(), removed_occurrence: source, }), std::collections::hash_map::Entry::Vacant(entry) => { entry.insert(source); canonical_sort.push(asc_desc); } } } AscDesc::Asc(Member::Geo(_)) | AscDesc::Desc(Member::Geo(_)) => match geo_sorted { Some(earlier_sort_index) => { canonicalization_actions.push(CanonicalizationAction::RemovedDuplicate { earlier_occurrence: RankingRuleSource::Sort { criterion_index, sort_index: earlier_sort_index, }, removed_occurrence: source, }) } None => { geo_sorted = Some(sort_index); canonical_sort.push(asc_desc); } }, } } canonical_sort } fn canonicalize_criterion( criterion: Criterion, criterion_index: usize, terms_matching_strategy: TermsMatchingStrategy, words: &mut Option>, canonicalization_actions: &mut Vec, canonical_criteria: &mut Vec, rule: &mut Option, ) { *words = match (terms_matching_strategy, words.take()) { (TermsMatchingStrategy::All, words) => words, (_, None) => { // inject words canonicalization_actions.push(CanonicalizationAction::PrependedWords { prepended_index: RankingRuleSource::Criterion(criterion_index), }); canonical_criteria.push(Criterion::Words); Some(None) } (_, words) => words, }; if let Some(previous_index) = *rule { canonicalization_actions.push(CanonicalizationAction::RemovedDuplicate { earlier_occurrence: RankingRuleSource::Criterion(previous_index), removed_occurrence: RankingRuleSource::Criterion(criterion_index), }); } else { *rule = Some(criterion_index); canonical_criteria.push(criterion) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum RankingRuleKind { Relevancy, AscendingSort, DescendingSort, AscendingGeoSort, DescendingGeoSort, } #[derive(Debug, Clone, Copy)] pub struct RankingRule { source: RankingRuleSource, kind: RankingRuleKind, } #[derive(Debug, Clone, Copy)] pub enum RankingRuleSource { Criterion(usize), CoalescedCriteria(usize, usize), Sort { criterion_index: usize, sort_index: usize }, } impl RankingRuleSource { fn rule_name(&self, criteria: &[Criterion], sort: &Option>) -> String { match self { RankingRuleSource::Criterion(criterion_index) => criteria .get(*criterion_index) .map(|c| c.to_string()) .unwrap_or_else(|| "unknown".into()), RankingRuleSource::CoalescedCriteria(begin, end) => { let rules: Vec<_> = criteria .get(*begin..=*end) .iter() .flat_map(|c| c.iter()) .map(|c| c.to_string()) .collect(); rules.join(", ") } RankingRuleSource::Sort { criterion_index: _, sort_index } => { match sort.as_deref().and_then(|sort| sort.get(*sort_index)) { Some(sort) => match sort { AscDesc::Asc(Member::Field(field_name)) => format!("{field_name}:asc"), AscDesc::Desc(Member::Field(field_name)) => { format!("{field_name}:desc") } AscDesc::Asc(Member::Geo(_)) => "_geo(..):asc".to_string(), AscDesc::Desc(Member::Geo(_)) => "_geo(..):desc".to_string(), }, None => "unknown".into(), } } } } fn rule_position(&self) -> String { match self { RankingRuleSource::Criterion(criterion_index) => { format!("#{criterion_index} in ranking rules") } RankingRuleSource::CoalescedCriteria(begin, end) => { format!("#{begin} to #{end} in ranking rules") } RankingRuleSource::Sort { criterion_index, sort_index } => format!( "#{sort_index} in `query.sort` (as `sort` is #{criterion_index} in ranking rules)" ), } } } impl RankingRule { fn from_criterion<'a>( criterion_index: usize, criterion: &'a Criterion, sort: &'a Option>, ) -> impl Iterator + 'a { let kind = match criterion { Criterion::Words | Criterion::Typo | Criterion::Proximity | Criterion::Attribute | Criterion::Exactness => RankingRuleKind::Relevancy, Criterion::Asc(s) if s == "_geo" => RankingRuleKind::AscendingGeoSort, Criterion::Asc(_) => RankingRuleKind::AscendingSort, Criterion::Desc(s) if s == "_geo" => RankingRuleKind::DescendingGeoSort, Criterion::Desc(_) => RankingRuleKind::DescendingSort, Criterion::Sort => { return either::Right(sort.iter().flatten().enumerate().map( move |(rule_index, asc_desc)| { Self::from_asc_desc(asc_desc, criterion_index, rule_index) }, )) } }; either::Left(std::iter::once(Self { source: RankingRuleSource::Criterion(criterion_index), kind, })) } fn from_asc_desc(asc_desc: &AscDesc, sort_index: usize, rule_index_in_sort: usize) -> Self { let kind = match asc_desc { AscDesc::Asc(Member::Field(_)) => RankingRuleKind::AscendingSort, AscDesc::Desc(Member::Field(_)) => RankingRuleKind::DescendingSort, AscDesc::Asc(Member::Geo(_)) => RankingRuleKind::AscendingGeoSort, AscDesc::Desc(Member::Geo(_)) => RankingRuleKind::DescendingGeoSort, }; Self { source: RankingRuleSource::Sort { criterion_index: sort_index, sort_index: rule_index_in_sort, }, kind, } } fn as_string( &self, canonical_criteria: &[Criterion], canonical_sort: &Option>, query_index: usize, index_uid: &str, ) -> String { let kind = match self.kind { RankingRuleKind::Relevancy => "relevancy", RankingRuleKind::AscendingSort => "ascending sort", RankingRuleKind::DescendingSort => "descending sort", RankingRuleKind::AscendingGeoSort => "ascending geo sort", RankingRuleKind::DescendingGeoSort => "descending geo sort", }; let rules = self.fetch_from_source(canonical_criteria, canonical_sort); let source = match self.source { RankingRuleSource::Criterion(criterion_index) => format!("`queries[{query_index}]`, `{index_uid}.rankingRules[{criterion_index}]`"), RankingRuleSource::CoalescedCriteria(begin, end) => format!("`queries[{query_index}]`, `{index_uid}.rankingRules[{begin}..={end}]`"), RankingRuleSource::Sort { criterion_index, sort_index } => format!("`queries[{query_index}].sort[{sort_index}]`, `{index_uid}.rankingRules[{criterion_index}]`"), }; format!("{source}: {kind} {rules}") } fn fetch_from_source( &self, canonical_criteria: &[Criterion], canonical_sort: &Option>, ) -> String { let rule_name = match self.source { RankingRuleSource::Criterion(index) => { canonical_criteria.get(index).map(|criterion| criterion.to_string()) } RankingRuleSource::CoalescedCriteria(begin, end) => { let rules: Vec = canonical_criteria .get(begin..=end) .into_iter() .flat_map(|criteria| criteria.iter()) .map(|criterion| criterion.to_string()) .collect(); (!rules.is_empty()).then_some(rules.join(", ")) } RankingRuleSource::Sort { criterion_index: _, sort_index } => canonical_sort .as_deref() .and_then(|canonical_sort| canonical_sort.get(sort_index)) .and_then(|asc_desc: &AscDesc| match asc_desc { AscDesc::Asc(Member::Field(s)) | AscDesc::Desc(Member::Field(s)) => { Some(format!("on field `{s}`")) } _ => None, }), }; let rule_name = rule_name.unwrap_or_else(|| "default".into()); format!("rule(s) {rule_name}") } }