diff --git a/crates/meilisearch-types/src/error.rs b/crates/meilisearch-types/src/error.rs index d2500b7e1..86f13add0 100644 --- a/crates/meilisearch-types/src/error.rs +++ b/crates/meilisearch-types/src/error.rs @@ -308,6 +308,10 @@ InvalidSearchShowRankingScoreDetails , InvalidRequest , BAD_REQU InvalidSimilarShowRankingScoreDetails , InvalidRequest , BAD_REQUEST ; InvalidSearchSort , InvalidRequest , BAD_REQUEST ; InvalidSearchDistinct , InvalidRequest , BAD_REQUEST ; +InvalidSearchPersonalization , InvalidRequest , BAD_REQUEST ; +InvalidSearchPersonalizationPersonalized , InvalidRequest , BAD_REQUEST ; +InvalidSearchPersonalizationUserProfile , InvalidRequest , BAD_REQUEST ; +InvalidSearchMediaAndVector , InvalidRequest , BAD_REQUEST ; InvalidSettingsDisplayedAttributes , InvalidRequest , BAD_REQUEST ; InvalidSettingsDistinctAttribute , InvalidRequest , BAD_REQUEST ; InvalidSettingsProximityPrecision , InvalidRequest , BAD_REQUEST ; @@ -636,6 +640,24 @@ impl fmt::Display for deserr_codes::InvalidNetworkSearchApiKey { } } +impl fmt::Display for deserr_codes::InvalidSearchPersonalization { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "the value of `personalization` is invalid, expected a JSON object with `personalized` boolean and optional `userProfile` string.") + } +} + +impl fmt::Display for deserr_codes::InvalidSearchPersonalizationPersonalized { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "the value of `personalized` is invalid, expected a boolean.") + } +} + +impl fmt::Display for deserr_codes::InvalidSearchPersonalizationUserProfile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "the value of `userProfile` is invalid, expected a string.") + } +} + #[macro_export] macro_rules! internal_error { ($target:ty : $($other:path), *) => { diff --git a/crates/meilisearch/src/routes/indexes/facet_search.rs b/crates/meilisearch/src/routes/indexes/facet_search.rs index 41f306746..84c8dd2b9 100644 --- a/crates/meilisearch/src/routes/indexes/facet_search.rs +++ b/crates/meilisearch/src/routes/indexes/facet_search.rs @@ -337,6 +337,7 @@ impl From for SearchQuery { hybrid, ranking_score_threshold, locales, + personalization: None, } } } diff --git a/crates/meilisearch/src/routes/indexes/search.rs b/crates/meilisearch/src/routes/indexes/search.rs index 333ae1944..81bd9f398 100644 --- a/crates/meilisearch/src/routes/indexes/search.rs +++ b/crates/meilisearch/src/routes/indexes/search.rs @@ -22,10 +22,10 @@ use crate::extractors::sequential_extractor::SeqHandler; use crate::metrics::MEILISEARCH_DEGRADED_SEARCH_REQUESTS; use crate::routes::indexes::search_analytics::{SearchAggregator, SearchGET, SearchPOST}; use crate::search::{ - add_search_rules, perform_search, HybridQuery, MatchingStrategy, RankingScoreThreshold, - RetrieveVectors, SearchKind, SearchQuery, SearchResult, SemanticRatio, DEFAULT_CROP_LENGTH, - DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG, - DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, DEFAULT_SEMANTIC_RATIO, + add_search_rules, perform_search, HybridQuery, MatchingStrategy, Personalization, + RankingScoreThreshold, RetrieveVectors, SearchKind, SearchQuery, SearchResult, SemanticRatio, + DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, + DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, DEFAULT_SEMANTIC_RATIO, }; use crate::search_queue::SearchQueue; @@ -132,6 +132,11 @@ pub struct SearchQueryGet { #[deserr(default, error = DeserrQueryParamError)] #[param(value_type = Vec, explode = false)] pub locales: Option>, + #[deserr(default, error = DeserrQueryParamError)] + #[param(value_type = bool)] + pub personalization_personalized: Option, + #[deserr(default, error = DeserrQueryParamError)] + pub personalization_user_profile: Option, } #[derive(Debug, Clone, Copy, PartialEq, deserr::Deserr)] @@ -203,6 +208,22 @@ impl TryFrom for SearchQuery { )); } + let personalization = match ( + other.personalization_personalized, + other.personalization_user_profile, + ) { + (None, None) => None, + (Some(personalized), user_profile) => { + Some(Personalization { personalized, user_profile }) + } + (None, Some(_)) => { + return Err(ResponseError::from_msg( + "`personalizationPersonalized` is mandatory when `personalizationUserProfile` is present".into(), + meilisearch_types::error::Code::InvalidSearchPersonalization, + )); + } + }; + Ok(Self { q: other.q, vector: other.vector.map(CS::into_inner), @@ -230,6 +251,7 @@ impl TryFrom for SearchQuery { hybrid, ranking_score_threshold: other.ranking_score_threshold.map(|o| o.0), locales: other.locales.map(|o| o.into_iter().collect()), + personalization, }) } } diff --git a/crates/meilisearch/src/routes/indexes/search_analytics.rs b/crates/meilisearch/src/routes/indexes/search_analytics.rs index b16e2636e..e92b5160a 100644 --- a/crates/meilisearch/src/routes/indexes/search_analytics.rs +++ b/crates/meilisearch/src/routes/indexes/search_analytics.rs @@ -125,6 +125,7 @@ impl SearchAggregator { hybrid, ranking_score_threshold, locales, + personalization: _, } = query; let mut ret = Self::default(); diff --git a/crates/meilisearch/src/routes/multi_search_analytics.rs b/crates/meilisearch/src/routes/multi_search_analytics.rs index 3fa23f630..96b5323b3 100644 --- a/crates/meilisearch/src/routes/multi_search_analytics.rs +++ b/crates/meilisearch/src/routes/multi_search_analytics.rs @@ -66,6 +66,7 @@ impl MultiSearchAggregator { hybrid: _, ranking_score_threshold: _, locales: _, + personalization: _, } in &federated_search.queries { if let Some(federation_options) = federation_options { diff --git a/crates/meilisearch/src/search/mod.rs b/crates/meilisearch/src/search/mod.rs index 5e543c53f..bf7e6c281 100644 --- a/crates/meilisearch/src/search/mod.rs +++ b/crates/meilisearch/src/search/mod.rs @@ -57,6 +57,15 @@ pub const DEFAULT_HIGHLIGHT_PRE_TAG: fn() -> String = || "".to_string(); pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "".to_string(); pub const DEFAULT_SEMANTIC_RATIO: fn() -> SemanticRatio = || SemanticRatio(0.5); +#[derive(Clone, Default, PartialEq, Deserr, ToSchema, Debug)] +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] +pub struct Personalization { + #[deserr(default, error = DeserrJsonError)] + pub personalized: bool, + #[deserr(default, error = DeserrJsonError)] + pub user_profile: Option, +} + #[derive(Clone, Default, PartialEq, Deserr, ToSchema)] #[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] pub struct SearchQuery { @@ -118,6 +127,8 @@ pub struct SearchQuery { pub ranking_score_threshold: Option, #[deserr(default, error = DeserrJsonError)] pub locales: Option>, + #[deserr(default, error = DeserrJsonError, default)] + pub personalization: Option, } impl From for SearchQuery { @@ -164,6 +175,7 @@ impl From for SearchQuery { highlight_post_tag: DEFAULT_HIGHLIGHT_POST_TAG(), crop_marker: DEFAULT_CROP_MARKER(), locales: None, + personalization: None, } } } @@ -244,6 +256,7 @@ impl fmt::Debug for SearchQuery { attributes_to_search_on, ranking_score_threshold, locales, + personalization, } = self; let mut debug = f.debug_struct("SearchQuery"); @@ -329,6 +342,10 @@ impl fmt::Debug for SearchQuery { debug.field("locales", &locales); } + if let Some(personalization) = personalization { + debug.field("personalization", &personalization); + } + debug.finish() } } @@ -531,6 +548,9 @@ pub struct SearchQueryWithIndex { pub ranking_score_threshold: Option, #[deserr(default, error = DeserrJsonError, default)] pub locales: Option>, + #[deserr(default, error = DeserrJsonError, default)] + #[serde(skip)] + pub personalization: Option, #[deserr(default)] pub federation_options: Option, @@ -587,6 +607,7 @@ impl SearchQueryWithIndex { attributes_to_search_on, ranking_score_threshold, locales, + personalization, } = query; SearchQueryWithIndex { @@ -617,6 +638,7 @@ impl SearchQueryWithIndex { attributes_to_search_on, ranking_score_threshold, locales, + personalization, federation_options, } } @@ -651,6 +673,7 @@ impl SearchQueryWithIndex { hybrid, ranking_score_threshold, locales, + personalization, } = self; ( index_uid, @@ -681,6 +704,7 @@ impl SearchQueryWithIndex { hybrid, ranking_score_threshold, locales, + personalization, // do not use ..Default::default() here, // rather add any missing field from `SearchQuery` to `SearchQueryWithIndex` }, @@ -905,7 +929,7 @@ pub struct SearchResultWithIndex { pub result: SearchResult, } -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)] +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)] #[serde(untagged)] pub enum HitsInfo { #[serde(rename_all = "camelCase")] @@ -1133,6 +1157,7 @@ pub fn perform_search( attributes_to_search_on: _, filter: _, distinct: _, + personalization: _, } = query; let format = AttributesFormat {