mirror of
https://github.com/meilisearch/meilisearch.git
synced 2025-03-06 22:02:34 +08:00
Change the filterableAttributes setting API
**Changes:** The filterableAttributes type has been changed from a `BTreeSet<String>` to a `Vec<FilterableAttributesRule>`, Which is a list of rules defining patterns to match the documents' fields and a set of feature to apply on the matching fields. The rule order given by the user is now an important information, the features applied on a filterable field will be chosen based on the rule order as we do for the LocalizedAttributesRules. This means that the list will not be reordered anymore and will keep the user defined order, moreover, if there are any duplicates, they will not be de-duplicated anymore. **Impact:** - Settings API - the database format of the filterable attributes changed - may impact the LocalizedAttributesRules due to the AttributePatterns factorization - OpenAPI generator
This commit is contained in:
parent
c63c25a9a2
commit
0200c65ebf
@ -1,5 +1,5 @@
|
|||||||
use deserr::Deserr;
|
use deserr::Deserr;
|
||||||
use milli::LocalizedAttributesRule;
|
use milli::{AttributePatterns, LocalizedAttributesRule};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
@ -7,7 +7,7 @@ use utoipa::ToSchema;
|
|||||||
#[deserr(rename_all = camelCase)]
|
#[deserr(rename_all = camelCase)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct LocalizedAttributesRuleView {
|
pub struct LocalizedAttributesRuleView {
|
||||||
pub attribute_patterns: Vec<String>,
|
pub attribute_patterns: AttributePatterns,
|
||||||
pub locales: Vec<Locale>,
|
pub locales: Vec<Locale>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ use fst::IntoStreamer;
|
|||||||
use milli::index::{IndexEmbeddingConfig, PrefixSearch};
|
use milli::index::{IndexEmbeddingConfig, PrefixSearch};
|
||||||
use milli::proximity::ProximityPrecision;
|
use milli::proximity::ProximityPrecision;
|
||||||
use milli::update::Setting;
|
use milli::update::Setting;
|
||||||
use milli::{Criterion, CriterionError, Index, DEFAULT_VALUES_PER_FACET};
|
use milli::{Criterion, CriterionError, FilterableAttributesRule, Index, DEFAULT_VALUES_PER_FACET};
|
||||||
use serde::{Deserialize, Serialize, Serializer};
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
@ -202,8 +202,8 @@ pub struct Settings<T> {
|
|||||||
/// Attributes to use for faceting and filtering. See [Filtering and Faceted Search](https://www.meilisearch.com/docs/learn/filtering_and_sorting/search_with_facet_filters).
|
/// Attributes to use for faceting and filtering. See [Filtering and Faceted Search](https://www.meilisearch.com/docs/learn/filtering_and_sorting/search_with_facet_filters).
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsFilterableAttributes>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsFilterableAttributes>)]
|
||||||
#[schema(value_type = Option<Vec<String>>, example = json!(["release_date", "genre"]))]
|
#[schema(value_type = Option<Vec<FilterableAttributesRule>>, example = json!(["release_date", "genre"]))]
|
||||||
pub filterable_attributes: Setting<BTreeSet<String>>,
|
pub filterable_attributes: Setting<Vec<FilterableAttributesRule>>,
|
||||||
/// Attributes to use when sorting search results.
|
/// Attributes to use when sorting search results.
|
||||||
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
#[serde(default, skip_serializing_if = "Setting::is_not_set")]
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSettingsSortableAttributes>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSettingsSortableAttributes>)]
|
||||||
@ -791,7 +791,7 @@ pub fn settings(
|
|||||||
.user_defined_searchable_fields(rtxn)?
|
.user_defined_searchable_fields(rtxn)?
|
||||||
.map(|fields| fields.into_iter().map(String::from).collect());
|
.map(|fields| fields.into_iter().map(String::from).collect());
|
||||||
|
|
||||||
let filterable_attributes = index.filterable_fields(rtxn)?.into_iter().collect();
|
let filterable_attributes = index.filterable_attributes_rules(rtxn)?.into_iter().collect();
|
||||||
|
|
||||||
let sortable_attributes = index.sortable_fields(rtxn)?.into_iter().collect();
|
let sortable_attributes = index.sortable_fields(rtxn)?.into_iter().collect();
|
||||||
|
|
||||||
|
@ -291,7 +291,7 @@ make_setting_routes!(
|
|||||||
{
|
{
|
||||||
route: "/filterable-attributes",
|
route: "/filterable-attributes",
|
||||||
update_verb: put,
|
update_verb: put,
|
||||||
value_type: std::collections::BTreeSet<String>,
|
value_type: Vec<meilisearch_types::milli::FilterableAttributesRule>,
|
||||||
err_type: meilisearch_types::deserr::DeserrJsonError<
|
err_type: meilisearch_types::deserr::DeserrJsonError<
|
||||||
meilisearch_types::error::deserr_codes::InvalidSettingsFilterableAttributes,
|
meilisearch_types::error::deserr_codes::InvalidSettingsFilterableAttributes,
|
||||||
>,
|
>,
|
||||||
|
@ -8,6 +8,7 @@ use std::collections::{BTreeMap, BTreeSet, HashSet};
|
|||||||
use meilisearch_types::facet_values_sort::FacetValuesSort;
|
use meilisearch_types::facet_values_sort::FacetValuesSort;
|
||||||
use meilisearch_types::locales::{Locale, LocalizedAttributesRuleView};
|
use meilisearch_types::locales::{Locale, LocalizedAttributesRuleView};
|
||||||
use meilisearch_types::milli::update::Setting;
|
use meilisearch_types::milli::update::Setting;
|
||||||
|
use meilisearch_types::milli::FilterableAttributesRule;
|
||||||
use meilisearch_types::settings::{
|
use meilisearch_types::settings::{
|
||||||
FacetingSettings, PaginationSettings, PrefixSearchSettings, ProximityPrecisionView,
|
FacetingSettings, PaginationSettings, PrefixSearchSettings, ProximityPrecisionView,
|
||||||
RankingRuleView, SettingEmbeddingSettings, TypoSettings,
|
RankingRuleView, SettingEmbeddingSettings, TypoSettings,
|
||||||
@ -89,6 +90,10 @@ impl Aggregate for SettingsAnalytics {
|
|||||||
filterable_attributes: FilterableAttributesAnalytics {
|
filterable_attributes: FilterableAttributesAnalytics {
|
||||||
total: new.filterable_attributes.total.or(self.filterable_attributes.total),
|
total: new.filterable_attributes.total.or(self.filterable_attributes.total),
|
||||||
has_geo: new.filterable_attributes.has_geo.or(self.filterable_attributes.has_geo),
|
has_geo: new.filterable_attributes.has_geo.or(self.filterable_attributes.has_geo),
|
||||||
|
has_patterns: new
|
||||||
|
.filterable_attributes
|
||||||
|
.has_patterns
|
||||||
|
.or(self.filterable_attributes.has_patterns),
|
||||||
},
|
},
|
||||||
distinct_attribute: DistinctAttributeAnalytics {
|
distinct_attribute: DistinctAttributeAnalytics {
|
||||||
set: self.distinct_attribute.set | new.distinct_attribute.set,
|
set: self.distinct_attribute.set | new.distinct_attribute.set,
|
||||||
@ -328,13 +333,19 @@ impl SortableAttributesAnalytics {
|
|||||||
pub struct FilterableAttributesAnalytics {
|
pub struct FilterableAttributesAnalytics {
|
||||||
pub total: Option<usize>,
|
pub total: Option<usize>,
|
||||||
pub has_geo: Option<bool>,
|
pub has_geo: Option<bool>,
|
||||||
|
pub has_patterns: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FilterableAttributesAnalytics {
|
impl FilterableAttributesAnalytics {
|
||||||
pub fn new(setting: Option<&BTreeSet<String>>) -> Self {
|
pub fn new(setting: Option<&Vec<FilterableAttributesRule>>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
total: setting.as_ref().map(|filter| filter.len()),
|
total: setting.as_ref().map(|filter| filter.len()),
|
||||||
has_geo: setting.as_ref().map(|filter| filter.contains("_geo")),
|
has_geo: setting
|
||||||
|
.as_ref()
|
||||||
|
.map(|filter| filter.iter().any(FilterableAttributesRule::has_geo)),
|
||||||
|
has_patterns: setting.as_ref().map(|filter| {
|
||||||
|
filter.iter().any(|rule| matches!(rule, FilterableAttributesRule::Pattern(_)))
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,10 @@ use meilisearch_types::batches::BatchStats;
|
|||||||
use meilisearch_types::error::{Code, ErrorType, ResponseError};
|
use meilisearch_types::error::{Code, ErrorType, ResponseError};
|
||||||
use meilisearch_types::index_uid::IndexUid;
|
use meilisearch_types::index_uid::IndexUid;
|
||||||
use meilisearch_types::keys::CreateApiKey;
|
use meilisearch_types::keys::CreateApiKey;
|
||||||
|
use meilisearch_types::milli::{
|
||||||
|
AttributePatterns, FilterFeatures, FilterableAttributesFeatures, FilterableAttributesPatterns,
|
||||||
|
FilterableAttributesRule,
|
||||||
|
};
|
||||||
use meilisearch_types::settings::{
|
use meilisearch_types::settings::{
|
||||||
Checked, FacetingSettings, MinWordSizeTyposSetting, PaginationSettings, Settings, TypoSettings,
|
Checked, FacetingSettings, MinWordSizeTyposSetting, PaginationSettings, Settings, TypoSettings,
|
||||||
Unchecked,
|
Unchecked,
|
||||||
@ -88,7 +92,7 @@ pub mod tasks;
|
|||||||
url = "/",
|
url = "/",
|
||||||
description = "Local server",
|
description = "Local server",
|
||||||
)),
|
)),
|
||||||
components(schemas(PaginationView<KeyView>, PaginationView<IndexView>, IndexView, DocumentDeletionByFilter, AllBatches, BatchStats, ProgressStepView, ProgressView, BatchView, RuntimeTogglableFeatures, SwapIndexesPayload, DocumentEditionByFunction, MergeFacets, FederationOptions, SearchQueryWithIndex, Federation, FederatedSearch, FederatedSearchResult, SearchResults, SearchResultWithIndex, SimilarQuery, SimilarResult, PaginationView<serde_json::Value>, BrowseQuery, UpdateIndexRequest, IndexUid, IndexCreateRequest, KeyView, Action, CreateApiKey, UpdateStderrLogs, LogMode, GetLogs, IndexStats, Stats, HealthStatus, HealthResponse, VersionResponse, Code, ErrorType, AllTasks, TaskView, Status, DetailsView, ResponseError, Settings<Unchecked>, Settings<Checked>, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings, SummarizedTaskView, Kind, Network, Remote))
|
components(schemas(PaginationView<KeyView>, PaginationView<IndexView>, IndexView, DocumentDeletionByFilter, AllBatches, BatchStats, ProgressStepView, ProgressView, BatchView, RuntimeTogglableFeatures, SwapIndexesPayload, DocumentEditionByFunction, MergeFacets, FederationOptions, SearchQueryWithIndex, Federation, FederatedSearch, FederatedSearchResult, SearchResults, SearchResultWithIndex, SimilarQuery, SimilarResult, PaginationView<serde_json::Value>, BrowseQuery, UpdateIndexRequest, IndexUid, IndexCreateRequest, KeyView, Action, CreateApiKey, UpdateStderrLogs, LogMode, GetLogs, IndexStats, Stats, HealthStatus, HealthResponse, VersionResponse, Code, ErrorType, AllTasks, TaskView, Status, DetailsView, ResponseError, Settings<Unchecked>, Settings<Checked>, TypoSettings, MinWordSizeTyposSetting, FacetingSettings, PaginationSettings, SummarizedTaskView, Kind, Network, Remote, FilterableAttributesRule, FilterableAttributesPatterns, AttributePatterns, FilterableAttributesFeatures, FilterFeatures))
|
||||||
)]
|
)]
|
||||||
pub struct MeilisearchApi;
|
pub struct MeilisearchApi;
|
||||||
|
|
||||||
|
128
crates/milli/src/attribute_patterns.rs
Normal file
128
crates/milli/src/attribute_patterns.rs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
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(value_type = Vec<String>)]
|
||||||
|
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 {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_pattern(pattern: &str, str: &str) -> PatternMatch {
|
||||||
|
if pattern == "*" {
|
||||||
|
return PatternMatch::Match;
|
||||||
|
} else if pattern.starts_with('*') && pattern.ends_with('*') {
|
||||||
|
if str.contains(&pattern[1..pattern.len() - 1]) {
|
||||||
|
return PatternMatch::Match;
|
||||||
|
}
|
||||||
|
} else if let Some(pattern) = pattern.strip_prefix('*') {
|
||||||
|
if str.ends_with(pattern) {
|
||||||
|
return PatternMatch::Match;
|
||||||
|
}
|
||||||
|
} else if let Some(pattern) = pattern.strip_suffix('*') {
|
||||||
|
if str.starts_with(pattern) {
|
||||||
|
return PatternMatch::Match;
|
||||||
|
}
|
||||||
|
} else if pattern == str {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 the of a nested field that matches the pattern
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
204
crates/milli/src/filterable_attributes_rules.rs
Normal file
204
crates/milli/src/filterable_attributes_rules.rs
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
use deserr::{DeserializeError, Deserr, ValuePointerRef};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::{BTreeSet, HashSet};
|
||||||
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
attribute_patterns::{match_distinct_field, match_field_legacy, PatternMatch},
|
||||||
|
constants::RESERVED_GEO_FIELD_NAME,
|
||||||
|
AttributePatterns, FieldsIdsMap,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, ToSchema)]
|
||||||
|
#[serde(untagged)]
|
||||||
|
pub enum FilterableAttributesRule {
|
||||||
|
Field(String),
|
||||||
|
Pattern(FilterableAttributesPatterns),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterableAttributesRule {
|
||||||
|
pub fn match_str(&self, field: &str) -> PatternMatch {
|
||||||
|
match self {
|
||||||
|
FilterableAttributesRule::Field(pattern) => match_field_legacy(pattern, field),
|
||||||
|
FilterableAttributesRule::Pattern(patterns) => patterns.match_str(field),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn has_geo(&self) -> bool {
|
||||||
|
matches!(self, FilterableAttributesRule::Field(field_name) if field_name == RESERVED_GEO_FIELD_NAME)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn features(&self) -> FilterableAttributesFeatures {
|
||||||
|
match self {
|
||||||
|
FilterableAttributesRule::Field(_) => FilterableAttributesFeatures::legacy_default(),
|
||||||
|
FilterableAttributesRule::Pattern(patterns) => patterns.features(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Deserr, ToSchema)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
|
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||||
|
pub struct FilterableAttributesPatterns {
|
||||||
|
pub patterns: AttributePatterns,
|
||||||
|
#[serde(default)]
|
||||||
|
#[deserr(default)]
|
||||||
|
pub features: FilterableAttributesFeatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterableAttributesPatterns {
|
||||||
|
pub fn match_str(&self, field: &str) -> PatternMatch {
|
||||||
|
self.patterns.match_str(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn features(&self) -> FilterableAttributesFeatures {
|
||||||
|
self.features.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Deserr, ToSchema)]
|
||||||
|
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||||
|
#[deserr(rename_all = camelCase, deny_unknown_fields)]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FilterableAttributesFeatures {
|
||||||
|
facet_search: bool,
|
||||||
|
filter: FilterFeatures,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterableAttributesFeatures {
|
||||||
|
pub fn legacy_default() -> Self {
|
||||||
|
Self { facet_search: true, filter: FilterFeatures::legacy_default() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn no_features() -> Self {
|
||||||
|
Self { facet_search: false, filter: FilterFeatures::no_features() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_filterable(&self) -> bool {
|
||||||
|
self.filter.is_filterable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if `IS EMPTY` is allowed
|
||||||
|
pub fn is_filterable_empty(&self) -> bool {
|
||||||
|
self.filter.is_filterable_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if `=` and `IN` are allowed
|
||||||
|
pub fn is_filterable_equality(&self) -> bool {
|
||||||
|
self.filter.is_filterable_equality()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if `IS NULL` is allowed
|
||||||
|
pub fn is_filterable_null(&self) -> bool {
|
||||||
|
self.filter.is_filterable_null()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if `IS EXISTS` is allowed
|
||||||
|
pub fn is_filterable_exists(&self) -> bool {
|
||||||
|
self.filter.is_filterable_exists()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if `<`, `>`, `<=`, `>=` or `TO` are allowed
|
||||||
|
pub fn is_filterable_comparison(&self) -> bool {
|
||||||
|
self.filter.is_filterable_comparison()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the facet search is allowed
|
||||||
|
pub fn is_facet_searchable(&self) -> bool {
|
||||||
|
self.facet_search
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn allowed_filter_operators(&self) -> Vec<String> {
|
||||||
|
self.filter.allowed_operators()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: DeserializeError> Deserr<E> for FilterableAttributesRule {
|
||||||
|
fn deserialize_from_value<V: deserr::IntoValue>(
|
||||||
|
value: deserr::Value<V>,
|
||||||
|
location: ValuePointerRef,
|
||||||
|
) -> Result<Self, E> {
|
||||||
|
if value.kind() == deserr::ValueKind::Map {
|
||||||
|
Ok(Self::Pattern(FilterableAttributesPatterns::deserialize_from_value(
|
||||||
|
value, location,
|
||||||
|
)?))
|
||||||
|
} else {
|
||||||
|
Ok(Self::Field(String::deserialize_from_value(value, location)?))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug, Deserr, ToSchema)]
|
||||||
|
pub struct FilterFeatures {
|
||||||
|
equality: bool,
|
||||||
|
comparison: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FilterFeatures {
|
||||||
|
pub fn allowed_operators(&self) -> Vec<String> {
|
||||||
|
if !self.is_filterable() {
|
||||||
|
return vec![];
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut operators = vec!["OR", "AND", "NOT"];
|
||||||
|
if self.is_filterable_equality() {
|
||||||
|
operators.extend_from_slice(&["=", "!=", "IN"]);
|
||||||
|
}
|
||||||
|
if self.is_filterable_comparison() {
|
||||||
|
operators.extend_from_slice(&["<", ">", "<=", ">=", "TO"]);
|
||||||
|
}
|
||||||
|
if self.is_filterable_empty() {
|
||||||
|
operators.push("IS EMPTY");
|
||||||
|
}
|
||||||
|
if self.is_filterable_null() {
|
||||||
|
operators.push("IS NULL");
|
||||||
|
}
|
||||||
|
if self.is_filterable_exists() {
|
||||||
|
operators.push("EXISTS");
|
||||||
|
}
|
||||||
|
|
||||||
|
operators.into_iter().map(String::from).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_filterable(&self) -> bool {
|
||||||
|
self.equality || self.comparison
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_filterable_equality(&self) -> bool {
|
||||||
|
self.equality
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if `<`, `>`, `<=`, `>=` or `TO` are allowed
|
||||||
|
pub fn is_filterable_comparison(&self) -> bool {
|
||||||
|
self.comparison
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if `IS EMPTY` is allowed
|
||||||
|
pub fn is_filterable_empty(&self) -> bool {
|
||||||
|
self.is_filterable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if `IS EXISTS` is allowed
|
||||||
|
pub fn is_filterable_exists(&self) -> bool {
|
||||||
|
self.is_filterable()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if `IS NULL` is allowed
|
||||||
|
pub fn is_filterable_null(&self) -> bool {
|
||||||
|
self.is_filterable()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn legacy_default() -> Self {
|
||||||
|
Self { equality: true, comparison: true }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn no_features() -> Self {
|
||||||
|
Self { equality: false, comparison: false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for FilterFeatures {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self { equality: true, comparison: false }
|
||||||
|
}
|
||||||
|
}
|
@ -876,11 +876,11 @@ impl Index {
|
|||||||
|
|
||||||
/* filterable fields */
|
/* filterable fields */
|
||||||
|
|
||||||
/// Writes the filterable fields names in the database.
|
/// Writes the filterable attributes rules in the database.
|
||||||
pub(crate) fn put_filterable_fields(
|
pub(crate) fn put_filterable_attributes_rules(
|
||||||
&self,
|
&self,
|
||||||
wtxn: &mut RwTxn<'_>,
|
wtxn: &mut RwTxn<'_>,
|
||||||
fields: &HashSet<String>,
|
#[allow(clippy::ptr_arg)] fields: &Vec<FilterableAttributesRule>,
|
||||||
) -> heed::Result<()> {
|
) -> heed::Result<()> {
|
||||||
self.main.remap_types::<Str, SerdeJson<_>>().put(
|
self.main.remap_types::<Str, SerdeJson<_>>().put(
|
||||||
wtxn,
|
wtxn,
|
||||||
@ -889,13 +889,19 @@ impl Index {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deletes the filterable fields ids in the database.
|
/// Deletes the filterable attributes rules in the database.
|
||||||
pub(crate) fn delete_filterable_fields(&self, wtxn: &mut RwTxn<'_>) -> heed::Result<bool> {
|
pub(crate) fn delete_filterable_attributes_rules(
|
||||||
|
&self,
|
||||||
|
wtxn: &mut RwTxn<'_>,
|
||||||
|
) -> heed::Result<bool> {
|
||||||
self.main.remap_key_type::<Str>().delete(wtxn, main_key::FILTERABLE_FIELDS_KEY)
|
self.main.remap_key_type::<Str>().delete(wtxn, main_key::FILTERABLE_FIELDS_KEY)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the filterable fields names.
|
/// Returns the filterable attributes rules.
|
||||||
pub fn filterable_fields(&self, rtxn: &RoTxn<'_>) -> heed::Result<HashSet<String>> {
|
pub fn filterable_attributes_rules(
|
||||||
|
&self,
|
||||||
|
rtxn: &RoTxn<'_>,
|
||||||
|
) -> heed::Result<Vec<FilterableAttributesRule>> {
|
||||||
Ok(self
|
Ok(self
|
||||||
.main
|
.main
|
||||||
.remap_types::<Str, SerdeJson<_>>()
|
.remap_types::<Str, SerdeJson<_>>()
|
||||||
@ -903,21 +909,6 @@ impl Index {
|
|||||||
.unwrap_or_default())
|
.unwrap_or_default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Identical to `filterable_fields`, but returns ids instead.
|
|
||||||
pub fn filterable_fields_ids(&self, rtxn: &RoTxn<'_>) -> Result<HashSet<FieldId>> {
|
|
||||||
let fields = self.filterable_fields(rtxn)?;
|
|
||||||
let fields_ids_map = self.fields_ids_map(rtxn)?;
|
|
||||||
|
|
||||||
let mut fields_ids = HashSet::new();
|
|
||||||
for name in fields {
|
|
||||||
if let Some(field_id) = fields_ids_map.id(&name) {
|
|
||||||
fields_ids.insert(field_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(fields_ids)
|
|
||||||
}
|
|
||||||
|
|
||||||
/* sortable fields */
|
/* sortable fields */
|
||||||
|
|
||||||
/// Writes the sortable fields names in the database.
|
/// Writes the sortable fields names in the database.
|
||||||
|
@ -9,12 +9,14 @@ pub static ALLOC: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|||||||
pub mod documents;
|
pub mod documents;
|
||||||
|
|
||||||
mod asc_desc;
|
mod asc_desc;
|
||||||
|
mod attribute_patterns;
|
||||||
mod criterion;
|
mod criterion;
|
||||||
pub mod database_stats;
|
pub mod database_stats;
|
||||||
mod error;
|
mod error;
|
||||||
mod external_documents_ids;
|
mod external_documents_ids;
|
||||||
pub mod facet;
|
pub mod facet;
|
||||||
mod fields_ids_map;
|
mod fields_ids_map;
|
||||||
|
mod filterable_attributes_rules;
|
||||||
pub mod heed_codec;
|
pub mod heed_codec;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
mod localized_attributes_rules;
|
mod localized_attributes_rules;
|
||||||
@ -52,6 +54,8 @@ pub use thread_pool_no_abort::{PanicCatched, ThreadPoolNoAbort, ThreadPoolNoAbor
|
|||||||
pub use {charabia as tokenizer, heed, rhai};
|
pub use {charabia as tokenizer, heed, rhai};
|
||||||
|
|
||||||
pub use self::asc_desc::{AscDesc, AscDescError, Member, SortError};
|
pub use self::asc_desc::{AscDesc, AscDescError, Member, SortError};
|
||||||
|
pub use self::attribute_patterns::AttributePatterns;
|
||||||
|
pub use self::attribute_patterns::PatternMatch;
|
||||||
pub use self::criterion::{default_criteria, Criterion, CriterionError};
|
pub use self::criterion::{default_criteria, Criterion, CriterionError};
|
||||||
pub use self::error::{
|
pub use self::error::{
|
||||||
Error, FieldIdMapMissingEntry, InternalError, SerializationError, UserError,
|
Error, FieldIdMapMissingEntry, InternalError, SerializationError, UserError,
|
||||||
@ -59,6 +63,10 @@ pub use self::error::{
|
|||||||
pub use self::external_documents_ids::ExternalDocumentsIds;
|
pub use self::external_documents_ids::ExternalDocumentsIds;
|
||||||
pub use self::fieldids_weights_map::FieldidsWeightsMap;
|
pub use self::fieldids_weights_map::FieldidsWeightsMap;
|
||||||
pub use self::fields_ids_map::{FieldsIdsMap, GlobalFieldsIdsMap};
|
pub use self::fields_ids_map::{FieldsIdsMap, GlobalFieldsIdsMap};
|
||||||
|
pub use self::filterable_attributes_rules::{
|
||||||
|
FilterFeatures, FilterableAttributesFeatures, FilterableAttributesPatterns,
|
||||||
|
FilterableAttributesRule,
|
||||||
|
};
|
||||||
pub use self::heed_codec::{
|
pub use self::heed_codec::{
|
||||||
BEU16StrCodec, BEU32StrCodec, BoRoaringBitmapCodec, BoRoaringBitmapLenCodec,
|
BEU16StrCodec, BEU32StrCodec, BoRoaringBitmapCodec, BoRoaringBitmapLenCodec,
|
||||||
CboRoaringBitmapCodec, CboRoaringBitmapLenCodec, FieldIdWordCountCodec, ObkvCodec,
|
CboRoaringBitmapCodec, CboRoaringBitmapLenCodec, FieldIdWordCountCodec, ObkvCodec,
|
||||||
@ -67,7 +75,6 @@ pub use self::heed_codec::{
|
|||||||
};
|
};
|
||||||
pub use self::index::Index;
|
pub use self::index::Index;
|
||||||
pub use self::localized_attributes_rules::LocalizedAttributesRule;
|
pub use self::localized_attributes_rules::LocalizedAttributesRule;
|
||||||
use self::localized_attributes_rules::LocalizedFieldIds;
|
|
||||||
pub use self::search::facet::{FacetValueHit, SearchForFacetValues};
|
pub use self::search::facet::{FacetValueHit, SearchForFacetValues};
|
||||||
pub use self::search::similar::Similar;
|
pub use self::search::similar::Similar;
|
||||||
pub use self::search::{
|
pub use self::search::{
|
||||||
|
@ -4,8 +4,9 @@ use charabia::Language;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use utoipa::ToSchema;
|
use utoipa::ToSchema;
|
||||||
|
|
||||||
|
use crate::attribute_patterns::PatternMatch;
|
||||||
use crate::fields_ids_map::FieldsIdsMap;
|
use crate::fields_ids_map::FieldsIdsMap;
|
||||||
use crate::FieldId;
|
use crate::{AttributePatterns, FieldId};
|
||||||
|
|
||||||
/// A rule that defines which locales are supported for a given attribute.
|
/// A rule that defines which locales are supported for a given attribute.
|
||||||
///
|
///
|
||||||
@ -17,18 +18,18 @@ use crate::FieldId;
|
|||||||
/// The pattern `*attribute_name*` matches any attribute name that contains `attribute_name`.
|
/// The pattern `*attribute_name*` matches any attribute name that contains `attribute_name`.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
|
||||||
pub struct LocalizedAttributesRule {
|
pub struct LocalizedAttributesRule {
|
||||||
pub attribute_patterns: Vec<String>,
|
pub attribute_patterns: AttributePatterns,
|
||||||
#[schema(value_type = Vec<String>)]
|
#[schema(value_type = Vec<String>)]
|
||||||
pub locales: Vec<Language>,
|
pub locales: Vec<Language>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LocalizedAttributesRule {
|
impl LocalizedAttributesRule {
|
||||||
pub fn new(attribute_patterns: Vec<String>, locales: Vec<Language>) -> Self {
|
pub fn new(attribute_patterns: Vec<String>, locales: Vec<Language>) -> Self {
|
||||||
Self { attribute_patterns, locales }
|
Self { attribute_patterns: AttributePatterns::from(attribute_patterns), locales }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn match_str(&self, str: &str) -> bool {
|
pub fn match_str(&self, str: &str) -> PatternMatch {
|
||||||
self.attribute_patterns.iter().any(|pattern| match_pattern(pattern.as_str(), str))
|
self.attribute_patterns.match_str(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn locales(&self) -> &[Language] {
|
pub fn locales(&self) -> &[Language] {
|
||||||
@ -36,20 +37,6 @@ impl LocalizedAttributesRule {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn match_pattern(pattern: &str, str: &str) -> bool {
|
|
||||||
if pattern == "*" {
|
|
||||||
true
|
|
||||||
} else if pattern.starts_with('*') && pattern.ends_with('*') {
|
|
||||||
str.contains(&pattern[1..pattern.len() - 1])
|
|
||||||
} else if let Some(pattern) = pattern.strip_prefix('*') {
|
|
||||||
str.ends_with(pattern)
|
|
||||||
} else if let Some(pattern) = pattern.strip_suffix('*') {
|
|
||||||
str.starts_with(pattern)
|
|
||||||
} else {
|
|
||||||
pattern == str
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct LocalizedFieldIds {
|
pub struct LocalizedFieldIds {
|
||||||
field_id_to_locales: HashMap<FieldId, Vec<Language>>,
|
field_id_to_locales: HashMap<FieldId, Vec<Language>>,
|
||||||
@ -65,13 +52,13 @@ impl LocalizedFieldIds {
|
|||||||
|
|
||||||
if let Some(rules) = rules {
|
if let Some(rules) = rules {
|
||||||
let fields = fields_ids.filter_map(|field_id| {
|
let fields = fields_ids.filter_map(|field_id| {
|
||||||
fields_ids_map.name(field_id).map(|field_name| (field_id, field_name))
|
fields_ids_map.name(field_id).map(|field_name: &str| (field_id, field_name))
|
||||||
});
|
});
|
||||||
|
|
||||||
for (field_id, field_name) in fields {
|
for (field_id, field_name) in fields {
|
||||||
let mut locales = Vec::new();
|
let mut locales = Vec::new();
|
||||||
for rule in rules {
|
for rule in rules {
|
||||||
if rule.match_str(field_name) {
|
if rule.match_str(field_name) == PatternMatch::Match {
|
||||||
locales.extend(rule.locales.iter());
|
locales.extend(rule.locales.iter());
|
||||||
// Take the first rule that matches
|
// Take the first rule that matches
|
||||||
break;
|
break;
|
||||||
@ -89,10 +76,6 @@ impl LocalizedFieldIds {
|
|||||||
Self { field_id_to_locales }
|
Self { field_id_to_locales }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn locales(&self, fields_id: FieldId) -> Option<&[Language]> {
|
|
||||||
self.field_id_to_locales.get(&fields_id).map(Vec::as_slice)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn all_locales(&self) -> Vec<Language> {
|
pub fn all_locales(&self) -> Vec<Language> {
|
||||||
let mut locales = Vec::new();
|
let mut locales = Vec::new();
|
||||||
for field_locales in self.field_id_to_locales.values() {
|
for field_locales in self.field_id_to_locales.values() {
|
||||||
@ -108,24 +91,3 @@ impl LocalizedFieldIds {
|
|||||||
locales
|
locales
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_match_pattern() {
|
|
||||||
assert!(match_pattern("*", "test"));
|
|
||||||
assert!(match_pattern("test*", "test"));
|
|
||||||
assert!(match_pattern("test*", "testa"));
|
|
||||||
assert!(match_pattern("*test", "test"));
|
|
||||||
assert!(match_pattern("*test", "atest"));
|
|
||||||
assert!(match_pattern("*test*", "test"));
|
|
||||||
assert!(match_pattern("*test*", "atesta"));
|
|
||||||
assert!(match_pattern("*test*", "atest"));
|
|
||||||
assert!(match_pattern("*test*", "testa"));
|
|
||||||
assert!(!match_pattern("test*test", "test"));
|
|
||||||
assert!(!match_pattern("*test", "testa"));
|
|
||||||
assert!(!match_pattern("test*", "atest"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user