Move the sortFacetValuesBy in the faceting settings

This commit is contained in:
Kerollmops 2023-06-22 17:13:40 +02:00 committed by Clément Renault
parent d9fea0143f
commit 9917bf046a
No known key found for this signature in database
GPG Key ID: 92ADA4E935E71FA4
11 changed files with 213 additions and 70 deletions

View File

@ -362,6 +362,7 @@ impl<T> From<v5::Settings<T>> for v6::Settings<v6::Unchecked> {
faceting: match settings.faceting { faceting: match settings.faceting {
v5::Setting::Set(faceting) => v6::Setting::Set(v6::FacetingSettings { v5::Setting::Set(faceting) => v6::Setting::Set(v6::FacetingSettings {
max_values_per_facet: faceting.max_values_per_facet.into(), 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::Reset => v6::Setting::Reset,
v5::Setting::NotSet => v6::Setting::NotSet, v5::Setting::NotSet => v6::Setting::NotSet,

View File

@ -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<FacetValuesSort> for OrderBy {
fn from(val: FacetValuesSort) -> Self {
match val {
FacetValuesSort::Alpha => OrderBy::Lexicographic,
FacetValuesSort::Count => OrderBy::Count,
}
}
}
impl From<OrderBy> for FacetValuesSort {
fn from(val: OrderBy) -> Self {
match val {
OrderBy::Lexicographic => FacetValuesSort::Alpha,
OrderBy::Count => FacetValuesSort::Count,
}
}
}

View File

@ -2,6 +2,7 @@ pub mod compression;
pub mod deserr; pub mod deserr;
pub mod document_formats; pub mod document_formats;
pub mod error; pub mod error;
pub mod facet_values_sort;
pub mod features; pub mod features;
pub mod index_uid; pub mod index_uid;
pub mod index_uid_pattern; pub mod index_uid_pattern;

View File

@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize, Serializer};
use crate::deserr::DeserrJsonError; use crate::deserr::DeserrJsonError;
use crate::error::deserr_codes::*; use crate::error::deserr_codes::*;
use crate::facet_values_sort::FacetValuesSort;
/// The maximimum number of results that the engine /// The maximimum number of results that the engine
/// will be able to return in one search call. /// 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")] #[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)] #[deserr(default)]
pub max_values_per_facet: Setting<usize>, pub max_values_per_facet: Setting<usize>,
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
#[deserr(default)]
pub sort_facet_values_by: Setting<BTreeMap<String, FacetValuesSort>>,
} }
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)] #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq, Deserr)]
@ -398,12 +402,21 @@ pub fn apply_settings_to_builder(
Setting::NotSet => (), Setting::NotSet => (),
} }
match settings.faceting { match &settings.faceting {
Setting::Set(ref value) => match value.max_values_per_facet { Setting::Set(FacetingSettings { max_values_per_facet, sort_facet_values_by }) => {
Setting::Set(val) => builder.set_max_values_per_facet(val), match max_values_per_facet {
Setting::Reset => builder.reset_max_values_per_facet(), Setting::Set(val) => builder.set_max_values_per_facet(*val),
Setting::NotSet => (), 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::Reset => builder.reset_max_values_per_facet(),
Setting::NotSet => (), Setting::NotSet => (),
} }
@ -476,6 +489,13 @@ pub fn settings(
max_values_per_facet: Setting::Set( max_values_per_facet: Setting::Set(
index.max_values_per_facet(rtxn)?.unwrap_or(DEFAULT_VALUES_PER_FACET), 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 { let pagination = PaginationSettings {

View File

@ -16,9 +16,9 @@ use crate::extractors::authentication::policies::*;
use crate::extractors::authentication::GuardedData; use crate::extractors::authentication::GuardedData;
use crate::extractors::sequential_extractor::SeqHandler; use crate::extractors::sequential_extractor::SeqHandler;
use crate::search::{ use crate::search::{
add_search_rules, perform_search, FacetValuesSort, MatchingStrategy, SearchQuery, add_search_rules, perform_search, MatchingStrategy, SearchQuery, DEFAULT_CROP_LENGTH,
DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG,
DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET,
}; };
pub fn configure(cfg: &mut web::ServiceConfig) { pub fn configure(cfg: &mut web::ServiceConfig) {
@ -64,8 +64,6 @@ pub struct SearchQueryGet {
show_ranking_score_details: Param<bool>, show_ranking_score_details: Param<bool>,
#[deserr(default, error = DeserrQueryParamError<InvalidSearchFacets>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchFacets>)]
facets: Option<CS<String>>, facets: Option<CS<String>>,
#[deserr(default, error = DeserrQueryParamError<InvalidSearchFacets>)] // TODO
sort_facet_values_by: Option<FacetValuesSort>,
#[deserr( default = DEFAULT_HIGHLIGHT_PRE_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPreTag>)] #[deserr( default = DEFAULT_HIGHLIGHT_PRE_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPreTag>)]
highlight_pre_tag: String, highlight_pre_tag: String,
#[deserr( default = DEFAULT_HIGHLIGHT_POST_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPostTag>)] #[deserr( default = DEFAULT_HIGHLIGHT_POST_TAG(), error = DeserrQueryParamError<InvalidSearchHighlightPostTag>)]
@ -105,7 +103,6 @@ impl From<SearchQueryGet> for SearchQuery {
show_ranking_score: other.show_ranking_score.0, show_ranking_score: other.show_ranking_score.0,
show_ranking_score_details: other.show_ranking_score_details.0, show_ranking_score_details: other.show_ranking_score_details.0,
facets: other.facets.map(|o| o.into_iter().collect()), 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_pre_tag: other.highlight_pre_tag,
highlight_post_tag: other.highlight_post_tag, highlight_post_tag: other.highlight_post_tag,
crop_marker: other.crop_marker, crop_marker: other.crop_marker,

View File

@ -407,6 +407,7 @@ make_setting_route!(
json!({ json!({
"faceting": { "faceting": {
"max_values_per_facet": setting.as_ref().and_then(|s| s.max_values_per_facet.set()), "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), Some(req),
@ -545,6 +546,10 @@ pub async fn update_all(
.as_ref() .as_ref()
.set() .set()
.and_then(|s| s.max_values_per_facet.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": { "pagination": {
"max_total_hits": new_settings.pagination "max_total_hits": new_settings.pagination

View File

@ -75,8 +75,6 @@ pub struct SearchQuery {
pub sort: Option<Vec<String>>, pub sort: Option<Vec<String>>,
#[deserr(default, error = DeserrJsonError<InvalidSearchFacets>)] #[deserr(default, error = DeserrJsonError<InvalidSearchFacets>)]
pub facets: Option<Vec<String>>, pub facets: Option<Vec<String>>,
#[deserr(default, error = DeserrJsonError<InvalidSearchFacets>)] // TODO
pub sort_facet_values_by: Option<FacetValuesSort>,
#[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())] #[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())]
pub highlight_pre_tag: String, pub highlight_pre_tag: String,
#[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())] #[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())]
@ -136,8 +134,6 @@ pub struct SearchQueryWithIndex {
pub sort: Option<Vec<String>>, pub sort: Option<Vec<String>>,
#[deserr(default, error = DeserrJsonError<InvalidSearchFacets>)] #[deserr(default, error = DeserrJsonError<InvalidSearchFacets>)]
pub facets: Option<Vec<String>>, pub facets: Option<Vec<String>>,
#[deserr(default, error = DeserrJsonError<InvalidSearchFacets>)] // TODO
pub sort_facet_values_by: Option<FacetValuesSort>,
#[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())] #[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPreTag>, default = DEFAULT_HIGHLIGHT_PRE_TAG())]
pub highlight_pre_tag: String, pub highlight_pre_tag: String,
#[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())] #[deserr(default, error = DeserrJsonError<InvalidSearchHighlightPostTag>, default = DEFAULT_HIGHLIGHT_POST_TAG())]
@ -170,7 +166,6 @@ impl SearchQueryWithIndex {
filter, filter,
sort, sort,
facets, facets,
sort_facet_values_by,
highlight_pre_tag, highlight_pre_tag,
highlight_post_tag, highlight_post_tag,
crop_marker, crop_marker,
@ -196,7 +191,6 @@ impl SearchQueryWithIndex {
filter, filter,
sort, sort,
facets, facets,
sort_facet_values_by,
highlight_pre_tag, highlight_pre_tag,
highlight_post_tag, highlight_post_tag,
crop_marker, crop_marker,
@ -581,12 +575,29 @@ pub fn perform_search(
.unwrap_or(DEFAULT_VALUES_PER_FACET); .unwrap_or(DEFAULT_VALUES_PER_FACET);
facet_distribution.max_values_per_facet(max_values_by_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 != "*") { 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); facet_distribution.facets(fields);
} }
let distribution = facet_distribution let distribution = facet_distribution
.candidates(candidates) .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()?; .execute()?;
let stats = facet_distribution.compute_stats()?; let stats = facet_distribution.compute_stats()?;
(Some(distribution), Some(stats)) (Some(distribution), Some(stats))

View File

@ -26,7 +26,8 @@ use crate::readable_slices::ReadableSlices;
use crate::{ use crate::{
default_criteria, CboRoaringBitmapCodec, Criterion, DocumentId, ExternalDocumentsIds, default_criteria, CboRoaringBitmapCodec, Criterion, DocumentId, ExternalDocumentsIds,
FacetDistribution, FieldDistribution, FieldId, FieldIdWordCountCodec, GeoPoint, ObkvCodec, 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. /// 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_WORDS: &str = "exact-words";
pub const EXACT_ATTRIBUTES: &str = "exact-attributes"; pub const EXACT_ATTRIBUTES: &str = "exact-attributes";
pub const MAX_VALUES_PER_FACET: &str = "max-values-per-facet"; 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"; 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) self.main.delete::<_, Str>(txn, main_key::MAX_VALUES_PER_FACET)
} }
pub fn sort_facet_values_by(&self, txn: &RoTxn) -> heed::Result<HashMap<String, OrderBy>> {
let mut orders = self
.main
.get::<_, Str, SerdeJson<HashMap<String, OrderBy>>>(
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<String, OrderBy>,
) -> 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<bool> {
self.main.delete::<_, Str>(txn, main_key::SORT_FACET_VALUES_BY)
}
pub fn pagination_max_total_hits(&self, txn: &RoTxn) -> heed::Result<Option<usize>> { pub fn pagination_max_total_hits(&self, txn: &RoTxn) -> heed::Result<Option<usize>> {
self.main.get::<_, Str, OwnedType<usize>>(txn, main_key::PAGINATION_MAX_TOTAL_HITS) self.main.get::<_, Str, OwnedType<usize>>(txn, main_key::PAGINATION_MAX_TOTAL_HITS)
} }

View File

@ -1,4 +1,4 @@
use std::collections::{BTreeMap, HashSet}; use std::collections::{BTreeMap, HashMap, HashSet};
use std::ops::ControlFlow; use std::ops::ControlFlow;
use std::{fmt, mem}; use std::{fmt, mem};
@ -6,6 +6,7 @@ use heed::types::ByteSlice;
use heed::BytesDecode; use heed::BytesDecode;
use indexmap::IndexMap; use indexmap::IndexMap;
use roaring::RoaringBitmap; use roaring::RoaringBitmap;
use serde::{Deserialize, Serialize};
use crate::error::UserError; use crate::error::UserError;
use crate::facet::FacetType; use crate::facet::FacetType;
@ -27,7 +28,7 @@ pub const DEFAULT_VALUES_PER_FACET: usize = 100;
const CANDIDATES_THRESHOLD: u64 = 3000; const CANDIDATES_THRESHOLD: u64 = 3000;
/// How should we fetch the facets? /// 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 { pub enum OrderBy {
/// By lexicographic order... /// By lexicographic order...
#[default] #[default]
@ -37,10 +38,10 @@ pub enum OrderBy {
} }
pub struct FacetDistribution<'a> { pub struct FacetDistribution<'a> {
facets: Option<HashSet<String>>, facets: Option<HashMap<String, OrderBy>>,
candidates: Option<RoaringBitmap>, candidates: Option<RoaringBitmap>,
max_values_per_facet: usize, max_values_per_facet: usize,
order_by: OrderBy, default_order_by: OrderBy,
rtxn: &'a heed::RoTxn<'a>, rtxn: &'a heed::RoTxn<'a>,
index: &'a Index, index: &'a Index,
} }
@ -51,14 +52,22 @@ impl<'a> FacetDistribution<'a> {
facets: None, facets: None,
candidates: None, candidates: None,
max_values_per_facet: DEFAULT_VALUES_PER_FACET, max_values_per_facet: DEFAULT_VALUES_PER_FACET,
order_by: OrderBy::default(), default_order_by: OrderBy::default(),
rtxn, rtxn,
index, index,
} }
} }
pub fn facets<I: IntoIterator<Item = A>, A: AsRef<str>>(&mut self, names: I) -> &mut Self { pub fn facets<I: IntoIterator<Item = (A, OrderBy)>, A: AsRef<str>>(
self.facets = Some(names.into_iter().map(|s| s.as_ref().to_string()).collect()); &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 self
} }
@ -67,8 +76,8 @@ impl<'a> FacetDistribution<'a> {
self self
} }
pub fn order_by(&mut self, order_by: OrderBy) -> &mut Self { pub fn default_order_by(&mut self, order_by: OrderBy) -> &mut Self {
self.order_by = order_by; self.default_order_by = order_by;
self self
} }
@ -220,11 +229,15 @@ impl<'a> FacetDistribution<'a> {
) )
} }
fn facet_values(&self, field_id: FieldId) -> heed::Result<IndexMap<String, u64>> { fn facet_values(
&self,
field_id: FieldId,
order_by: OrderBy,
) -> heed::Result<IndexMap<String, u64>> {
use FacetType::{Number, String}; use FacetType::{Number, String};
let mut distribution = IndexMap::new(); 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 => { (OrderBy::Lexicographic, Some(cnd)) if cnd.len() <= CANDIDATES_THRESHOLD => {
// Classic search, candidates were specified, we must return facet values only related // 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. // 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( self.facet_numbers_distribution_from_facet_levels(
field_id, field_id,
candidates, candidates,
self.order_by, order_by,
&mut distribution, &mut distribution,
)?; )?;
self.facet_strings_distribution_from_facet_levels( self.facet_strings_distribution_from_facet_levels(
field_id, field_id,
candidates, candidates,
self.order_by, order_by,
&mut distribution, &mut distribution,
)?; )?;
} }
@ -273,6 +286,7 @@ impl<'a> FacetDistribution<'a> {
Some(facets) => { Some(facets) => {
let invalid_fields: HashSet<_> = facets let invalid_fields: HashSet<_> = facets
.iter() .iter()
.map(|(name, _)| name)
.filter(|facet| !crate::is_faceted(facet, &filterable_fields)) .filter(|facet| !crate::is_faceted(facet, &filterable_fields))
.collect(); .collect();
if !invalid_fields.is_empty() { if !invalid_fields.is_empty() {
@ -282,7 +296,7 @@ impl<'a> FacetDistribution<'a> {
} }
.into()); .into());
} else { } else {
facets.clone() facets.into_iter().map(|(name, _)| name).cloned().collect()
} }
} }
None => filterable_fields, None => filterable_fields,
@ -327,6 +341,7 @@ impl<'a> FacetDistribution<'a> {
Some(ref facets) => { Some(ref facets) => {
let invalid_fields: HashSet<_> = facets let invalid_fields: HashSet<_> = facets
.iter() .iter()
.map(|(name, _)| name)
.filter(|facet| !crate::is_faceted(facet, &filterable_fields)) .filter(|facet| !crate::is_faceted(facet, &filterable_fields))
.collect(); .collect();
if !invalid_fields.is_empty() { if !invalid_fields.is_empty() {
@ -336,7 +351,7 @@ impl<'a> FacetDistribution<'a> {
} }
.into()); .into());
} else { } else {
facets.clone() facets.into_iter().map(|(name, _)| name).cloned().collect()
} }
} }
None => filterable_fields, None => filterable_fields,
@ -345,7 +360,13 @@ impl<'a> FacetDistribution<'a> {
let mut distribution = BTreeMap::new(); let mut distribution = BTreeMap::new();
for (fid, name) in fields_ids_map.iter() { for (fid, name) in fields_ids_map.iter() {
if crate::is_faceted(name, &fields) { 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); distribution.insert(name.to_string(), values);
} }
} }
@ -360,7 +381,7 @@ impl fmt::Debug for FacetDistribution<'_> {
facets, facets,
candidates, candidates,
max_values_per_facet, max_values_per_facet,
order_by, default_order_by,
rtxn: _, rtxn: _,
index: _, index: _,
} = self; } = self;
@ -369,7 +390,7 @@ impl fmt::Debug for FacetDistribution<'_> {
.field("facets", facets) .field("facets", facets)
.field("candidates", candidates) .field("candidates", candidates)
.field("max_values_per_facet", max_values_per_facet) .field("max_values_per_facet", max_values_per_facet)
.field("order_by", order_by) .field("default_order_by", default_order_by)
.finish() .finish()
} }
} }
@ -381,7 +402,7 @@ mod tests {
use crate::documents::documents_batch_reader_from_objects; use crate::documents::documents_batch_reader_from_objects;
use crate::index::tests::TempIndex; use crate::index::tests::TempIndex;
use crate::{milli_snap, FacetDistribution}; use crate::{milli_snap, FacetDistribution, OrderBy};
#[test] #[test]
fn few_candidates_few_facet_values() { fn few_candidates_few_facet_values() {
@ -406,14 +427,14 @@ mod tests {
let txn = index.read_txn().unwrap(); let txn = index.read_txn().unwrap();
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.execute() .execute()
.unwrap(); .unwrap();
milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2, "RED": 1}}"###); milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2, "RED": 1}}"###);
let map = FacetDistribution::new(&txn, &index) 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()) .candidates([0, 1, 2].iter().copied().collect())
.execute() .execute()
.unwrap(); .unwrap();
@ -421,7 +442,7 @@ mod tests {
milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2, "RED": 1}}"###); milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2, "RED": 1}}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates([1, 2].iter().copied().collect()) .candidates([1, 2].iter().copied().collect())
.execute() .execute()
.unwrap(); .unwrap();
@ -432,7 +453,7 @@ mod tests {
milli_snap!(format!("{map:?}"), @r###"{"colour": {" blue": 1, "RED": 1}}"###); milli_snap!(format!("{map:?}"), @r###"{"colour": {" blue": 1, "RED": 1}}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates([2].iter().copied().collect()) .candidates([2].iter().copied().collect())
.execute() .execute()
.unwrap(); .unwrap();
@ -440,7 +461,7 @@ mod tests {
milli_snap!(format!("{map:?}"), @r###"{"colour": {"RED": 1}}"###); milli_snap!(format!("{map:?}"), @r###"{"colour": {"RED": 1}}"###);
let map = FacetDistribution::new(&txn, &index) 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()) .candidates([0, 1, 2].iter().copied().collect())
.max_values_per_facet(1) .max_values_per_facet(1)
.execute() .execute()
@ -478,14 +499,14 @@ mod tests {
let txn = index.read_txn().unwrap(); let txn = index.read_txn().unwrap();
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.execute() .execute()
.unwrap(); .unwrap();
milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 4000, "Red": 6000}}"###); milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 4000, "Red": 6000}}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.max_values_per_facet(1) .max_values_per_facet(1)
.execute() .execute()
.unwrap(); .unwrap();
@ -493,7 +514,7 @@ mod tests {
milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 4000}}"###); milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 4000}}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((0..10_000).collect()) .candidates((0..10_000).collect())
.execute() .execute()
.unwrap(); .unwrap();
@ -501,7 +522,7 @@ mod tests {
milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 4000, "Red": 6000}}"###); milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 4000, "Red": 6000}}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((0..5_000).collect()) .candidates((0..5_000).collect())
.execute() .execute()
.unwrap(); .unwrap();
@ -509,7 +530,7 @@ mod tests {
milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2000, "Red": 3000}}"###); milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2000, "Red": 3000}}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((0..5_000).collect()) .candidates((0..5_000).collect())
.execute() .execute()
.unwrap(); .unwrap();
@ -517,7 +538,7 @@ mod tests {
milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2000, "Red": 3000}}"###); milli_snap!(format!("{map:?}"), @r###"{"colour": {"Blue": 2000, "Red": 3000}}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((0..5_000).collect()) .candidates((0..5_000).collect())
.max_values_per_facet(1) .max_values_per_facet(1)
.execute() .execute()
@ -555,14 +576,14 @@ mod tests {
let txn = index.read_txn().unwrap(); let txn = index.read_txn().unwrap();
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.execute() .execute()
.unwrap(); .unwrap();
milli_snap!(format!("{map:?}"), "no_candidates", @"ac9229ed5964d893af96a7076e2f8af5"); milli_snap!(format!("{map:?}"), "no_candidates", @"ac9229ed5964d893af96a7076e2f8af5");
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.max_values_per_facet(2) .max_values_per_facet(2)
.execute() .execute()
.unwrap(); .unwrap();
@ -570,7 +591,7 @@ mod tests {
milli_snap!(format!("{map:?}"), "no_candidates_with_max_2", @r###"{"colour": {"0": 10, "1": 10}}"###); milli_snap!(format!("{map:?}"), "no_candidates_with_max_2", @r###"{"colour": {"0": 10, "1": 10}}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((0..10_000).collect()) .candidates((0..10_000).collect())
.execute() .execute()
.unwrap(); .unwrap();
@ -578,7 +599,7 @@ mod tests {
milli_snap!(format!("{map:?}"), "candidates_0_10_000", @"ac9229ed5964d893af96a7076e2f8af5"); milli_snap!(format!("{map:?}"), "candidates_0_10_000", @"ac9229ed5964d893af96a7076e2f8af5");
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((0..5_000).collect()) .candidates((0..5_000).collect())
.execute() .execute()
.unwrap(); .unwrap();
@ -615,14 +636,14 @@ mod tests {
let txn = index.read_txn().unwrap(); let txn = index.read_txn().unwrap();
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.compute_stats() .compute_stats()
.unwrap(); .unwrap();
milli_snap!(format!("{map:?}"), "no_candidates", @"{}"); milli_snap!(format!("{map:?}"), "no_candidates", @"{}");
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((0..1000).collect()) .candidates((0..1000).collect())
.compute_stats() .compute_stats()
.unwrap(); .unwrap();
@ -630,7 +651,7 @@ mod tests {
milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 999.0)}"###); milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 999.0)}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((217..777).collect()) .candidates((217..777).collect())
.compute_stats() .compute_stats()
.unwrap(); .unwrap();
@ -667,14 +688,14 @@ mod tests {
let txn = index.read_txn().unwrap(); let txn = index.read_txn().unwrap();
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.compute_stats() .compute_stats()
.unwrap(); .unwrap();
milli_snap!(format!("{map:?}"), "no_candidates", @"{}"); milli_snap!(format!("{map:?}"), "no_candidates", @"{}");
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((0..1000).collect()) .candidates((0..1000).collect())
.compute_stats() .compute_stats()
.unwrap(); .unwrap();
@ -682,7 +703,7 @@ mod tests {
milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 1999.0)}"###); milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 1999.0)}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((217..777).collect()) .candidates((217..777).collect())
.compute_stats() .compute_stats()
.unwrap(); .unwrap();
@ -719,14 +740,14 @@ mod tests {
let txn = index.read_txn().unwrap(); let txn = index.read_txn().unwrap();
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.compute_stats() .compute_stats()
.unwrap(); .unwrap();
milli_snap!(format!("{map:?}"), "no_candidates", @"{}"); milli_snap!(format!("{map:?}"), "no_candidates", @"{}");
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((0..1000).collect()) .candidates((0..1000).collect())
.compute_stats() .compute_stats()
.unwrap(); .unwrap();
@ -734,7 +755,7 @@ mod tests {
milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 999.0)}"###); milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 999.0)}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((217..777).collect()) .candidates((217..777).collect())
.compute_stats() .compute_stats()
.unwrap(); .unwrap();
@ -775,14 +796,14 @@ mod tests {
let txn = index.read_txn().unwrap(); let txn = index.read_txn().unwrap();
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.compute_stats() .compute_stats()
.unwrap(); .unwrap();
milli_snap!(format!("{map:?}"), "no_candidates", @"{}"); milli_snap!(format!("{map:?}"), "no_candidates", @"{}");
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((0..1000).collect()) .candidates((0..1000).collect())
.compute_stats() .compute_stats()
.unwrap(); .unwrap();
@ -790,7 +811,7 @@ mod tests {
milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 1998.0)}"###); milli_snap!(format!("{map:?}"), "candidates_0_1000", @r###"{"colour": (0.0, 1998.0)}"###);
let map = FacetDistribution::new(&txn, &index) let map = FacetDistribution::new(&txn, &index)
.facets(std::iter::once("colour")) .facets(std::iter::once(("colour", OrderBy::default())))
.candidates((217..777).collect()) .candidates((217..777).collect())
.compute_stats() .compute_stats()
.unwrap(); .unwrap();

View File

@ -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::index::{DEFAULT_MIN_WORD_LEN_ONE_TYPO, DEFAULT_MIN_WORD_LEN_TWO_TYPOS};
use crate::update::index_documents::IndexDocumentsMethod; use crate::update::index_documents::IndexDocumentsMethod;
use crate::update::{IndexDocuments, UpdateIndexingStep}; use crate::update::{IndexDocuments, UpdateIndexingStep};
use crate::{FieldsIdsMap, Index, Result}; use crate::{FieldsIdsMap, Index, OrderBy, Result};
#[derive(Debug, Clone, PartialEq, Eq, Copy)] #[derive(Debug, Clone, PartialEq, Eq, Copy)]
pub enum Setting<T> { pub enum Setting<T> {
@ -122,6 +122,7 @@ pub struct Settings<'a, 't, 'u, 'i> {
/// Attributes on which typo tolerance is disabled. /// Attributes on which typo tolerance is disabled.
exact_attributes: Setting<HashSet<String>>, exact_attributes: Setting<HashSet<String>>,
max_values_per_facet: Setting<usize>, max_values_per_facet: Setting<usize>,
sort_facet_values_by: Setting<HashMap<String, OrderBy>>,
pagination_max_total_hits: Setting<usize>, pagination_max_total_hits: Setting<usize>,
} }
@ -149,6 +150,7 @@ impl<'a, 't, 'u, 'i> Settings<'a, 't, 'u, 'i> {
min_word_len_one_typo: Setting::NotSet, min_word_len_one_typo: Setting::NotSet,
exact_attributes: Setting::NotSet, exact_attributes: Setting::NotSet,
max_values_per_facet: Setting::NotSet, max_values_per_facet: Setting::NotSet,
sort_facet_values_by: Setting::NotSet,
pagination_max_total_hits: Setting::NotSet, pagination_max_total_hits: Setting::NotSet,
indexer_config, indexer_config,
} }
@ -275,6 +277,14 @@ impl<'a, 't, 'u, 'i> Settings<'a, 't, 'u, 'i> {
self.max_values_per_facet = Setting::Reset; self.max_values_per_facet = Setting::Reset;
} }
pub fn set_sort_facet_values_by(&mut self, value: HashMap<String, OrderBy>) {
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) { pub fn set_pagination_max_total_hits(&mut self, value: usize) {
self.pagination_max_total_hits = Setting::Set(value); self.pagination_max_total_hits = Setting::Set(value);
} }
@ -680,6 +690,20 @@ impl<'a, 't, 'u, 'i> Settings<'a, 't, 'u, 'i> {
Ok(()) 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<()> { fn update_pagination_max_total_hits(&mut self) -> Result<()> {
match self.pagination_max_total_hits { match self.pagination_max_total_hits {
Setting::Set(max) => { 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_min_typo_word_len()?;
self.update_exact_words()?; self.update_exact_words()?;
self.update_max_values_per_facet()?; self.update_max_values_per_facet()?;
self.update_sort_facet_values_by()?;
self.update_pagination_max_total_hits()?; self.update_pagination_max_total_hits()?;
// If there is new faceted fields we indicate that we must reindex as we must // If there is new faceted fields we indicate that we must reindex as we must
@ -1515,6 +1540,7 @@ mod tests {
exact_words, exact_words,
exact_attributes, exact_attributes,
max_values_per_facet, max_values_per_facet,
sort_facet_values_by,
pagination_max_total_hits, pagination_max_total_hits,
} = settings; } = settings;
assert!(matches!(searchable_fields, Setting::NotSet)); assert!(matches!(searchable_fields, Setting::NotSet));
@ -1532,6 +1558,7 @@ mod tests {
assert!(matches!(exact_words, Setting::NotSet)); assert!(matches!(exact_words, Setting::NotSet));
assert!(matches!(exact_attributes, Setting::NotSet)); assert!(matches!(exact_attributes, Setting::NotSet));
assert!(matches!(max_values_per_facet, 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)); assert!(matches!(pagination_max_total_hits, Setting::NotSet));
}) })
.unwrap(); .unwrap();

View File

@ -5,7 +5,7 @@ use heed::EnvOpenOptions;
use maplit::hashset; use maplit::hashset;
use milli::documents::{DocumentsBatchBuilder, DocumentsBatchReader}; use milli::documents::{DocumentsBatchBuilder, DocumentsBatchReader};
use milli::update::{IndexDocuments, IndexDocumentsConfig, IndexerConfig, Settings}; use milli::update::{IndexDocuments, IndexDocumentsConfig, IndexerConfig, Settings};
use milli::{FacetDistribution, Index, Object}; use milli::{FacetDistribution, Index, Object, OrderBy};
use serde_json::Deserializer; use serde_json::Deserializer;
#[test] #[test]
@ -63,12 +63,12 @@ fn test_facet_distribution_with_no_facet_values() {
let txn = index.read_txn().unwrap(); let txn = index.read_txn().unwrap();
let mut distrib = FacetDistribution::new(&txn, &index); let mut distrib = FacetDistribution::new(&txn, &index);
distrib.facets(vec!["genres"]); distrib.facets(vec![("genres", OrderBy::default())]);
let result = distrib.execute().unwrap(); let result = distrib.execute().unwrap();
assert_eq!(result["genres"].len(), 0); assert_eq!(result["genres"].len(), 0);
let mut distrib = FacetDistribution::new(&txn, &index); let mut distrib = FacetDistribution::new(&txn, &index);
distrib.facets(vec!["tags"]); distrib.facets(vec![("tags", OrderBy::default())]);
let result = distrib.execute().unwrap(); let result = distrib.execute().unwrap();
assert_eq!(result["tags"].len(), 2); assert_eq!(result["tags"].len(), 2);
} }