diff --git a/dump/src/reader/compat/v5_to_v6.rs b/dump/src/reader/compat/v5_to_v6.rs index b4c851d28..ef5588d8f 100644 --- a/dump/src/reader/compat/v5_to_v6.rs +++ b/dump/src/reader/compat/v5_to_v6.rs @@ -362,6 +362,7 @@ impl From> for v6::Settings { faceting: match settings.faceting { v5::Setting::Set(faceting) => v6::Setting::Set(v6::FacetingSettings { max_values_per_facet: faceting.max_values_per_facet.into(), + sort_facet_values_by: v6::Setting::NotSet, }), v5::Setting::Reset => v6::Setting::Reset, v5::Setting::NotSet => v6::Setting::NotSet, diff --git a/meilisearch-types/src/facet_values_sort.rs b/meilisearch-types/src/facet_values_sort.rs new file mode 100644 index 000000000..278061f19 --- /dev/null +++ b/meilisearch-types/src/facet_values_sort.rs @@ -0,0 +1,33 @@ +use deserr::Deserr; +use milli::OrderBy; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize, Deserr)] +#[serde(rename_all = "camelCase")] +#[deserr(rename_all = camelCase)] +pub enum FacetValuesSort { + /// Facet values are sorted in alphabetical order, ascending from A to Z. + #[default] + Alpha, + /// Facet values are sorted by decreasing count. + /// The count is the number of records containing this facet value in the results of the query. + Count, +} + +impl From for OrderBy { + fn from(val: FacetValuesSort) -> Self { + match val { + FacetValuesSort::Alpha => OrderBy::Lexicographic, + FacetValuesSort::Count => OrderBy::Count, + } + } +} + +impl From for FacetValuesSort { + fn from(val: OrderBy) -> Self { + match val { + OrderBy::Lexicographic => FacetValuesSort::Alpha, + OrderBy::Count => FacetValuesSort::Count, + } + } +} diff --git a/meilisearch-types/src/lib.rs b/meilisearch-types/src/lib.rs index dbdec14fc..b0762563a 100644 --- a/meilisearch-types/src/lib.rs +++ b/meilisearch-types/src/lib.rs @@ -2,6 +2,7 @@ pub mod compression; pub mod deserr; pub mod document_formats; pub mod error; +pub mod facet_values_sort; pub mod features; pub mod index_uid; pub mod index_uid_pattern; diff --git a/meilisearch-types/src/settings.rs b/meilisearch-types/src/settings.rs index 0fc7ea6e2..fa47a3dd7 100644 --- a/meilisearch-types/src/settings.rs +++ b/meilisearch-types/src/settings.rs @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize, Serializer}; use crate::deserr::DeserrJsonError; use crate::error::deserr_codes::*; +use crate::facet_values_sort::FacetValuesSort; /// The maximimum number of results that the engine /// will be able to return in one search call. @@ -102,6 +103,9 @@ pub struct FacetingSettings { #[serde(default, skip_serializing_if = "Setting::is_not_set")] #[deserr(default)] pub max_values_per_facet: Setting, + #[serde(default, skip_serializing_if = "Setting::is_not_set")] + #[deserr(default)] + pub sort_facet_values_by: Setting>, } #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] @@ -398,12 +402,21 @@ pub fn apply_settings_to_builder( Setting::NotSet => (), } - match settings.faceting { - Setting::Set(ref value) => match value.max_values_per_facet { - Setting::Set(val) => builder.set_max_values_per_facet(val), - Setting::Reset => builder.reset_max_values_per_facet(), - Setting::NotSet => (), - }, + match &settings.faceting { + Setting::Set(FacetingSettings { max_values_per_facet, sort_facet_values_by }) => { + match max_values_per_facet { + Setting::Set(val) => builder.set_max_values_per_facet(*val), + Setting::Reset => builder.reset_max_values_per_facet(), + Setting::NotSet => (), + } + match sort_facet_values_by { + Setting::Set(val) => builder.set_sort_facet_values_by( + val.iter().map(|(name, order)| (name.clone(), (*order).into())).collect(), + ), + Setting::Reset => builder.reset_sort_facet_values_by(), + Setting::NotSet => (), + } + } Setting::Reset => builder.reset_max_values_per_facet(), Setting::NotSet => (), } @@ -476,6 +489,13 @@ pub fn settings( max_values_per_facet: Setting::Set( index.max_values_per_facet(rtxn)?.unwrap_or(DEFAULT_VALUES_PER_FACET), ), + sort_facet_values_by: Setting::Set( + index + .sort_facet_values_by(rtxn)? + .into_iter() + .map(|(name, sort)| (name, sort.into())) + .collect(), + ), }; let pagination = PaginationSettings { diff --git a/meilisearch/src/routes/indexes/search.rs b/meilisearch/src/routes/indexes/search.rs index a25c83961..a79f95ee4 100644 --- a/meilisearch/src/routes/indexes/search.rs +++ b/meilisearch/src/routes/indexes/search.rs @@ -16,9 +16,9 @@ use crate::extractors::authentication::policies::*; use crate::extractors::authentication::GuardedData; use crate::extractors::sequential_extractor::SeqHandler; use crate::search::{ - add_search_rules, perform_search, FacetValuesSort, MatchingStrategy, SearchQuery, - DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, - DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, + add_search_rules, perform_search, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH, + DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, + DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, }; pub fn configure(cfg: &mut web::ServiceConfig) { @@ -64,8 +64,6 @@ pub struct SearchQueryGet { show_ranking_score_details: Param, #[deserr(default, error = DeserrQueryParamError)] facets: Option>, - #[deserr(default, error = DeserrQueryParamError)] // TODO - sort_facet_values_by: Option, #[deserr( default = DEFAULT_HIGHLIGHT_PRE_TAG(), error = DeserrQueryParamError)] highlight_pre_tag: String, #[deserr( default = DEFAULT_HIGHLIGHT_POST_TAG(), error = DeserrQueryParamError)] @@ -105,7 +103,6 @@ impl From for SearchQuery { show_ranking_score: other.show_ranking_score.0, show_ranking_score_details: other.show_ranking_score_details.0, facets: other.facets.map(|o| o.into_iter().collect()), - sort_facet_values_by: other.sort_facet_values_by, highlight_pre_tag: other.highlight_pre_tag, highlight_post_tag: other.highlight_post_tag, crop_marker: other.crop_marker, diff --git a/meilisearch/src/routes/indexes/settings.rs b/meilisearch/src/routes/indexes/settings.rs index 4b6cde685..7035093eb 100644 --- a/meilisearch/src/routes/indexes/settings.rs +++ b/meilisearch/src/routes/indexes/settings.rs @@ -407,6 +407,7 @@ make_setting_route!( json!({ "faceting": { "max_values_per_facet": setting.as_ref().and_then(|s| s.max_values_per_facet.set()), + "sort_facet_values_by": setting.as_ref().and_then(|s| s.sort_facet_values_by.clone().set()), }, }), Some(req), @@ -545,6 +546,10 @@ pub async fn update_all( .as_ref() .set() .and_then(|s| s.max_values_per_facet.as_ref().set()), + "sort_facet_values_by": new_settings.faceting + .as_ref() + .set() + .and_then(|s| s.sort_facet_values_by.as_ref().set()), }, "pagination": { "max_total_hits": new_settings.pagination diff --git a/meilisearch/src/search.rs b/meilisearch/src/search.rs index 6d614ad7b..5c45e030d 100644 --- a/meilisearch/src/search.rs +++ b/meilisearch/src/search.rs @@ -75,8 +75,6 @@ pub struct SearchQuery { pub sort: Option>, #[deserr(default, error = DeserrJsonError)] pub facets: Option>, - #[deserr(default, error = DeserrJsonError)] // TODO - pub sort_facet_values_by: Option, #[deserr(default, error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] pub highlight_pre_tag: String, #[deserr(default, error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_POST_TAG())] @@ -136,8 +134,6 @@ pub struct SearchQueryWithIndex { pub sort: Option>, #[deserr(default, error = DeserrJsonError)] pub facets: Option>, - #[deserr(default, error = DeserrJsonError)] // TODO - pub sort_facet_values_by: Option, #[deserr(default, error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_PRE_TAG())] pub highlight_pre_tag: String, #[deserr(default, error = DeserrJsonError, default = DEFAULT_HIGHLIGHT_POST_TAG())] @@ -170,7 +166,6 @@ impl SearchQueryWithIndex { filter, sort, facets, - sort_facet_values_by, highlight_pre_tag, highlight_post_tag, crop_marker, @@ -196,7 +191,6 @@ impl SearchQueryWithIndex { filter, sort, facets, - sort_facet_values_by, highlight_pre_tag, highlight_post_tag, crop_marker, @@ -581,12 +575,29 @@ pub fn perform_search( .unwrap_or(DEFAULT_VALUES_PER_FACET); facet_distribution.max_values_per_facet(max_values_by_facet); + let sort_facet_values_by = + index.sort_facet_values_by(&rtxn).map_err(milli::Error::from)?; + let default_sort_facet_values_by = + sort_facet_values_by.get("*").copied().unwrap_or_default(); + if fields.iter().all(|f| f != "*") { + let fields: Vec<_> = fields + .into_iter() + .map(|n| { + ( + n, + sort_facet_values_by + .get(n) + .copied() + .unwrap_or(default_sort_facet_values_by), + ) + }) + .collect(); facet_distribution.facets(fields); } let distribution = facet_distribution .candidates(candidates) - .order_by(query.sort_facet_values_by.map_or_else(Default::default, Into::into)) + .default_order_by(default_sort_facet_values_by) .execute()?; let stats = facet_distribution.compute_stats()?; (Some(distribution), Some(stats)) diff --git a/milli/src/index.rs b/milli/src/index.rs index 5c32f75f5..392ed1705 100644 --- a/milli/src/index.rs +++ b/milli/src/index.rs @@ -26,7 +26,8 @@ use crate::readable_slices::ReadableSlices; use crate::{ default_criteria, CboRoaringBitmapCodec, Criterion, DocumentId, ExternalDocumentsIds, FacetDistribution, FieldDistribution, FieldId, FieldIdWordCountCodec, GeoPoint, ObkvCodec, - Result, RoaringBitmapCodec, RoaringBitmapLenCodec, Search, U8StrStrCodec, BEU16, BEU32, + OrderBy, Result, RoaringBitmapCodec, RoaringBitmapLenCodec, Search, U8StrStrCodec, BEU16, + BEU32, }; /// The HNSW data-structure that we serialize, fill and search in. @@ -71,6 +72,7 @@ pub mod main_key { pub const EXACT_WORDS: &str = "exact-words"; pub const EXACT_ATTRIBUTES: &str = "exact-attributes"; pub const MAX_VALUES_PER_FACET: &str = "max-values-per-facet"; + pub const SORT_FACET_VALUES_BY: &str = "sort-facet-values-by"; pub const PAGINATION_MAX_TOTAL_HITS: &str = "pagination-max-total-hits"; } @@ -1298,6 +1300,31 @@ impl Index { self.main.delete::<_, Str>(txn, main_key::MAX_VALUES_PER_FACET) } + pub fn sort_facet_values_by(&self, txn: &RoTxn) -> heed::Result> { + let mut orders = self + .main + .get::<_, Str, SerdeJson>>( + txn, + main_key::SORT_FACET_VALUES_BY, + )? + .unwrap_or_default(); + // Insert the default ordering if it is not already overwritten by the user. + orders.entry("*".to_string()).or_insert(OrderBy::Lexicographic); + Ok(orders) + } + + pub(crate) fn put_sort_facet_values_by( + &self, + txn: &mut RwTxn, + val: &HashMap, + ) -> heed::Result<()> { + self.main.put::<_, Str, SerdeJson<_>>(txn, main_key::SORT_FACET_VALUES_BY, &val) + } + + pub(crate) fn delete_sort_facet_values_by(&self, txn: &mut RwTxn) -> heed::Result { + self.main.delete::<_, Str>(txn, main_key::SORT_FACET_VALUES_BY) + } + pub fn pagination_max_total_hits(&self, txn: &RoTxn) -> heed::Result> { self.main.get::<_, Str, OwnedType>(txn, main_key::PAGINATION_MAX_TOTAL_HITS) } diff --git a/milli/src/search/facet/facet_distribution.rs b/milli/src/search/facet/facet_distribution.rs index f49cbe1c4..fa6cc2fa0 100644 --- a/milli/src/search/facet/facet_distribution.rs +++ b/milli/src/search/facet/facet_distribution.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashSet}; +use std::collections::{BTreeMap, HashMap, HashSet}; use std::ops::ControlFlow; use std::{fmt, mem}; @@ -6,6 +6,7 @@ use heed::types::ByteSlice; use heed::BytesDecode; use indexmap::IndexMap; use roaring::RoaringBitmap; +use serde::{Deserialize, Serialize}; use crate::error::UserError; use crate::facet::FacetType; @@ -27,7 +28,7 @@ pub const DEFAULT_VALUES_PER_FACET: usize = 100; const CANDIDATES_THRESHOLD: u64 = 3000; /// How should we fetch the facets? -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] pub enum OrderBy { /// By lexicographic order... #[default] @@ -37,10 +38,10 @@ pub enum OrderBy { } pub struct FacetDistribution<'a> { - facets: Option>, + facets: Option>, candidates: Option, max_values_per_facet: usize, - order_by: OrderBy, + default_order_by: OrderBy, rtxn: &'a heed::RoTxn<'a>, index: &'a Index, } @@ -51,14 +52,22 @@ impl<'a> FacetDistribution<'a> { facets: None, candidates: None, max_values_per_facet: DEFAULT_VALUES_PER_FACET, - order_by: OrderBy::default(), + default_order_by: OrderBy::default(), rtxn, index, } } - pub fn facets, A: AsRef>(&mut self, names: I) -> &mut Self { - self.facets = Some(names.into_iter().map(|s| s.as_ref().to_string()).collect()); + pub fn facets, A: AsRef>( + &mut self, + names_ordered_by: I, + ) -> &mut Self { + self.facets = Some( + names_ordered_by + .into_iter() + .map(|(name, order_by)| (name.as_ref().to_string(), order_by)) + .collect(), + ); self } @@ -67,8 +76,8 @@ impl<'a> FacetDistribution<'a> { self } - pub fn order_by(&mut self, order_by: OrderBy) -> &mut Self { - self.order_by = order_by; + pub fn default_order_by(&mut self, order_by: OrderBy) -> &mut Self { + self.default_order_by = order_by; self } @@ -220,11 +229,15 @@ impl<'a> FacetDistribution<'a> { ) } - fn facet_values(&self, field_id: FieldId) -> heed::Result> { + fn facet_values( + &self, + field_id: FieldId, + order_by: OrderBy, + ) -> heed::Result> { use FacetType::{Number, String}; let mut distribution = IndexMap::new(); - match (self.order_by, &self.candidates) { + match (order_by, &self.candidates) { (OrderBy::Lexicographic, Some(cnd)) if cnd.len() <= CANDIDATES_THRESHOLD => { // Classic search, candidates were specified, we must return facet values only related // to those candidates. We also enter here for facet strings for performance reasons. @@ -245,13 +258,13 @@ impl<'a> FacetDistribution<'a> { self.facet_numbers_distribution_from_facet_levels( field_id, candidates, - self.order_by, + order_by, &mut distribution, )?; self.facet_strings_distribution_from_facet_levels( field_id, candidates, - self.order_by, + order_by, &mut distribution, )?; } @@ -273,6 +286,7 @@ impl<'a> FacetDistribution<'a> { Some(facets) => { let invalid_fields: HashSet<_> = facets .iter() + .map(|(name, _)| name) .filter(|facet| !crate::is_faceted(facet, &filterable_fields)) .collect(); if !invalid_fields.is_empty() { @@ -282,7 +296,7 @@ impl<'a> FacetDistribution<'a> { } .into()); } else { - facets.clone() + facets.into_iter().map(|(name, _)| name).cloned().collect() } } None => filterable_fields, @@ -327,6 +341,7 @@ impl<'a> FacetDistribution<'a> { Some(ref facets) => { let invalid_fields: HashSet<_> = facets .iter() + .map(|(name, _)| name) .filter(|facet| !crate::is_faceted(facet, &filterable_fields)) .collect(); if !invalid_fields.is_empty() { @@ -336,7 +351,7 @@ impl<'a> FacetDistribution<'a> { } .into()); } else { - facets.clone() + facets.into_iter().map(|(name, _)| name).cloned().collect() } } None => filterable_fields, @@ -345,7 +360,13 @@ impl<'a> FacetDistribution<'a> { let mut distribution = BTreeMap::new(); for (fid, name) in fields_ids_map.iter() { if crate::is_faceted(name, &fields) { - let values = self.facet_values(fid)?; + let order_by = self + .facets + .as_ref() + .map(|facets| facets.get(name).copied()) + .flatten() + .unwrap_or(self.default_order_by); + let values = self.facet_values(fid, order_by)?; distribution.insert(name.to_string(), values); } } @@ -360,7 +381,7 @@ impl fmt::Debug for FacetDistribution<'_> { facets, candidates, max_values_per_facet, - order_by, + default_order_by, rtxn: _, index: _, } = self; @@ -369,7 +390,7 @@ impl fmt::Debug for FacetDistribution<'_> { .field("facets", facets) .field("candidates", candidates) .field("max_values_per_facet", max_values_per_facet) - .field("order_by", order_by) + .field("default_order_by", default_order_by) .finish() } } @@ -381,7 +402,7 @@ mod tests { use crate::documents::documents_batch_reader_from_objects; use crate::index::tests::TempIndex; - use crate::{milli_snap, FacetDistribution}; + use crate::{milli_snap, FacetDistribution, OrderBy}; #[test] fn few_candidates_few_facet_values() { @@ -406,14 +427,14 @@ mod tests { let txn = index.read_txn().unwrap(); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .execute() .unwrap(); milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2, "RED": 1}}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates([0, 1, 2].iter().copied().collect()) .execute() .unwrap(); @@ -421,7 +442,7 @@ mod tests { milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2, "RED": 1}}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates([1, 2].iter().copied().collect()) .execute() .unwrap(); @@ -432,7 +453,7 @@ mod tests { milli_snap!(format!("{map:?}"), @r###"{"colour": {" blue": 1, "RED": 1}}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates([2].iter().copied().collect()) .execute() .unwrap(); @@ -440,7 +461,7 @@ mod tests { milli_snap!(format!("{map:?}"), @r###"{"colour": {"RED": 1}}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates([0, 1, 2].iter().copied().collect()) .max_values_per_facet(1) .execute() @@ -478,14 +499,14 @@ mod tests { let txn = index.read_txn().unwrap(); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .execute() .unwrap(); milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 4000, "Red": 6000}}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .max_values_per_facet(1) .execute() .unwrap(); @@ -493,7 +514,7 @@ mod tests { milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 4000}}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((0..10_000).collect()) .execute() .unwrap(); @@ -501,7 +522,7 @@ mod tests { milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 4000, "Red": 6000}}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((0..5_000).collect()) .execute() .unwrap(); @@ -509,7 +530,7 @@ mod tests { milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2000, "Red": 3000}}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((0..5_000).collect()) .execute() .unwrap(); @@ -517,7 +538,7 @@ mod tests { milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2000, "Red": 3000}}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((0..5_000).collect()) .max_values_per_facet(1) .execute() @@ -555,14 +576,14 @@ mod tests { let txn = index.read_txn().unwrap(); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .execute() .unwrap(); milli_snap!(format!("{map:?}"), "no_candidates", @"ac9229ed5964d893af96a7076e2f8af5"); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .max_values_per_facet(2) .execute() .unwrap(); @@ -570,7 +591,7 @@ mod tests { milli_snap!(format!("{map:?}"), "no_candidates_with_max_2", @r###"{"colour": {"0": 10, "1": 10}}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((0..10_000).collect()) .execute() .unwrap(); @@ -578,7 +599,7 @@ mod tests { milli_snap!(format!("{map:?}"), "candidates_0_10_000", @"ac9229ed5964d893af96a7076e2f8af5"); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((0..5_000).collect()) .execute() .unwrap(); @@ -615,14 +636,14 @@ mod tests { let txn = index.read_txn().unwrap(); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .compute_stats() .unwrap(); milli_snap!(format!("{map:?}"), "no_candidates", @"{}"); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((0..1000).collect()) .compute_stats() .unwrap(); @@ -630,7 +651,7 @@ mod tests { milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 999.0)}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((217..777).collect()) .compute_stats() .unwrap(); @@ -667,14 +688,14 @@ mod tests { let txn = index.read_txn().unwrap(); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .compute_stats() .unwrap(); milli_snap!(format!("{map:?}"), "no_candidates", @"{}"); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((0..1000).collect()) .compute_stats() .unwrap(); @@ -682,7 +703,7 @@ mod tests { milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 1999.0)}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((217..777).collect()) .compute_stats() .unwrap(); @@ -719,14 +740,14 @@ mod tests { let txn = index.read_txn().unwrap(); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .compute_stats() .unwrap(); milli_snap!(format!("{map:?}"), "no_candidates", @"{}"); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((0..1000).collect()) .compute_stats() .unwrap(); @@ -734,7 +755,7 @@ mod tests { milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 999.0)}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((217..777).collect()) .compute_stats() .unwrap(); @@ -775,14 +796,14 @@ mod tests { let txn = index.read_txn().unwrap(); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .compute_stats() .unwrap(); milli_snap!(format!("{map:?}"), "no_candidates", @"{}"); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((0..1000).collect()) .compute_stats() .unwrap(); @@ -790,7 +811,7 @@ mod tests { milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 1998.0)}"###); let map = FacetDistribution::new(&txn, &index) - .facets(std::iter::once("colour")) + .facets(std::iter::once(("colour", OrderBy::default()))) .candidates((217..777).collect()) .compute_stats() .unwrap(); diff --git a/milli/src/update/settings.rs b/milli/src/update/settings.rs index 3e271924b..aa69abca1 100644 --- a/milli/src/update/settings.rs +++ b/milli/src/update/settings.rs @@ -14,7 +14,7 @@ use crate::error::UserError; use crate::index::{DEFAULT_MIN_WORD_LEN_ONE_TYPO, DEFAULT_MIN_WORD_LEN_TWO_TYPOS}; use crate::update::index_documents::IndexDocumentsMethod; use crate::update::{IndexDocuments, UpdateIndexingStep}; -use crate::{FieldsIdsMap, Index, Result}; +use crate::{FieldsIdsMap, Index, OrderBy, Result}; #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub enum Setting { @@ -122,6 +122,7 @@ pub struct Settings<'a, 't, 'u, 'i> { /// Attributes on which typo tolerance is disabled. exact_attributes: Setting>, max_values_per_facet: Setting, + sort_facet_values_by: Setting>, pagination_max_total_hits: Setting, } @@ -149,6 +150,7 @@ impl<'a, 't, 'u, 'i> Settings<'a, 't, 'u, 'i> { min_word_len_one_typo: Setting::NotSet, exact_attributes: Setting::NotSet, max_values_per_facet: Setting::NotSet, + sort_facet_values_by: Setting::NotSet, pagination_max_total_hits: Setting::NotSet, indexer_config, } @@ -275,6 +277,14 @@ impl<'a, 't, 'u, 'i> Settings<'a, 't, 'u, 'i> { self.max_values_per_facet = Setting::Reset; } + pub fn set_sort_facet_values_by(&mut self, value: HashMap) { + self.sort_facet_values_by = Setting::Set(value); + } + + pub fn reset_sort_facet_values_by(&mut self) { + self.sort_facet_values_by = Setting::Reset; + } + pub fn set_pagination_max_total_hits(&mut self, value: usize) { self.pagination_max_total_hits = Setting::Set(value); } @@ -680,6 +690,20 @@ impl<'a, 't, 'u, 'i> Settings<'a, 't, 'u, 'i> { Ok(()) } + fn update_sort_facet_values_by(&mut self) -> Result<()> { + match self.sort_facet_values_by.as_ref() { + Setting::Set(value) => { + self.index.put_sort_facet_values_by(self.wtxn, value)?; + } + Setting::Reset => { + self.index.delete_sort_facet_values_by(self.wtxn)?; + } + Setting::NotSet => (), + } + + Ok(()) + } + fn update_pagination_max_total_hits(&mut self) -> Result<()> { match self.pagination_max_total_hits { Setting::Set(max) => { @@ -714,6 +738,7 @@ impl<'a, 't, 'u, 'i> Settings<'a, 't, 'u, 'i> { self.update_min_typo_word_len()?; self.update_exact_words()?; self.update_max_values_per_facet()?; + self.update_sort_facet_values_by()?; self.update_pagination_max_total_hits()?; // If there is new faceted fields we indicate that we must reindex as we must @@ -1515,6 +1540,7 @@ mod tests { exact_words, exact_attributes, max_values_per_facet, + sort_facet_values_by, pagination_max_total_hits, } = settings; assert!(matches!(searchable_fields, Setting::NotSet)); @@ -1532,6 +1558,7 @@ mod tests { assert!(matches!(exact_words, Setting::NotSet)); assert!(matches!(exact_attributes, Setting::NotSet)); assert!(matches!(max_values_per_facet, Setting::NotSet)); + assert!(matches!(sort_facet_values_by, Setting::NotSet)); assert!(matches!(pagination_max_total_hits, Setting::NotSet)); }) .unwrap(); diff --git a/milli/tests/search/facet_distribution.rs b/milli/tests/search/facet_distribution.rs index e2f89f2db..03405531d 100644 --- a/milli/tests/search/facet_distribution.rs +++ b/milli/tests/search/facet_distribution.rs @@ -5,7 +5,7 @@ use heed::EnvOpenOptions; use maplit::hashset; use milli::documents::{DocumentsBatchBuilder, DocumentsBatchReader}; use milli::update::{IndexDocuments, IndexDocumentsConfig, IndexerConfig, Settings}; -use milli::{FacetDistribution, Index, Object}; +use milli::{FacetDistribution, Index, Object, OrderBy}; use serde_json::Deserializer; #[test] @@ -63,12 +63,12 @@ fn test_facet_distribution_with_no_facet_values() { let txn = index.read_txn().unwrap(); let mut distrib = FacetDistribution::new(&txn, &index); - distrib.facets(vec!["genres"]); + distrib.facets(vec![("genres", OrderBy::default())]); let result = distrib.execute().unwrap(); assert_eq!(result["genres"].len(), 0); let mut distrib = FacetDistribution::new(&txn, &index); - distrib.facets(vec!["tags"]); + distrib.facets(vec![("tags", OrderBy::default())]); let result = distrib.execute().unwrap(); assert_eq!(result["tags"].len(), 2); }