mirror of
https://github.com/meilisearch/meilisearch.git
synced 2025-09-06 12:46:31 +00:00
feat: add personalization parameters to /search route
- Add Personalization struct with personalized boolean and user_profile string - Add personalizationPersonalized and personalizationUserProfile query parameters to SearchQueryGet - Follow same pattern as hybrid parameters (hybridEmbedder, hybridSemanticRatio) - Add validation: personalizationUserProfile requires personalizationPersonalized - Add error codes for personalization parameters - Update analytics and facet search to handle new personalization field - Remove serde dependencies from Personalization struct, use Deserr only
This commit is contained in:
@ -308,6 +308,10 @@ InvalidSearchShowRankingScoreDetails , InvalidRequest , BAD_REQU
|
|||||||
InvalidSimilarShowRankingScoreDetails , InvalidRequest , BAD_REQUEST ;
|
InvalidSimilarShowRankingScoreDetails , InvalidRequest , BAD_REQUEST ;
|
||||||
InvalidSearchSort , InvalidRequest , BAD_REQUEST ;
|
InvalidSearchSort , InvalidRequest , BAD_REQUEST ;
|
||||||
InvalidSearchDistinct , 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 ;
|
InvalidSettingsDisplayedAttributes , InvalidRequest , BAD_REQUEST ;
|
||||||
InvalidSettingsDistinctAttribute , InvalidRequest , BAD_REQUEST ;
|
InvalidSettingsDistinctAttribute , InvalidRequest , BAD_REQUEST ;
|
||||||
InvalidSettingsProximityPrecision , 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_export]
|
||||||
macro_rules! internal_error {
|
macro_rules! internal_error {
|
||||||
($target:ty : $($other:path), *) => {
|
($target:ty : $($other:path), *) => {
|
||||||
|
@ -337,6 +337,7 @@ impl From<FacetSearchQuery> for SearchQuery {
|
|||||||
hybrid,
|
hybrid,
|
||||||
ranking_score_threshold,
|
ranking_score_threshold,
|
||||||
locales,
|
locales,
|
||||||
|
personalization: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -22,10 +22,10 @@ use crate::extractors::sequential_extractor::SeqHandler;
|
|||||||
use crate::metrics::MEILISEARCH_DEGRADED_SEARCH_REQUESTS;
|
use crate::metrics::MEILISEARCH_DEGRADED_SEARCH_REQUESTS;
|
||||||
use crate::routes::indexes::search_analytics::{SearchAggregator, SearchGET, SearchPOST};
|
use crate::routes::indexes::search_analytics::{SearchAggregator, SearchGET, SearchPOST};
|
||||||
use crate::search::{
|
use crate::search::{
|
||||||
add_search_rules, perform_search, HybridQuery, MatchingStrategy, RankingScoreThreshold,
|
add_search_rules, perform_search, HybridQuery, MatchingStrategy, Personalization,
|
||||||
RetrieveVectors, SearchKind, SearchQuery, SearchResult, SemanticRatio, DEFAULT_CROP_LENGTH,
|
RankingScoreThreshold, RetrieveVectors, SearchKind, SearchQuery, SearchResult, SemanticRatio,
|
||||||
DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_HIGHLIGHT_PRE_TAG,
|
DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG,
|
||||||
DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, DEFAULT_SEMANTIC_RATIO,
|
DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, DEFAULT_SEMANTIC_RATIO,
|
||||||
};
|
};
|
||||||
use crate::search_queue::SearchQueue;
|
use crate::search_queue::SearchQueue;
|
||||||
|
|
||||||
@ -132,6 +132,11 @@ pub struct SearchQueryGet {
|
|||||||
#[deserr(default, error = DeserrQueryParamError<InvalidSearchLocales>)]
|
#[deserr(default, error = DeserrQueryParamError<InvalidSearchLocales>)]
|
||||||
#[param(value_type = Vec<Locale>, explode = false)]
|
#[param(value_type = Vec<Locale>, explode = false)]
|
||||||
pub locales: Option<CS<Locale>>,
|
pub locales: Option<CS<Locale>>,
|
||||||
|
#[deserr(default, error = DeserrQueryParamError<InvalidSearchPersonalizationPersonalized>)]
|
||||||
|
#[param(value_type = bool)]
|
||||||
|
pub personalization_personalized: Option<bool>,
|
||||||
|
#[deserr(default, error = DeserrQueryParamError<InvalidSearchPersonalizationUserProfile>)]
|
||||||
|
pub personalization_user_profile: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, deserr::Deserr)]
|
#[derive(Debug, Clone, Copy, PartialEq, deserr::Deserr)]
|
||||||
@ -203,6 +208,22 @@ impl TryFrom<SearchQueryGet> 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 {
|
Ok(Self {
|
||||||
q: other.q,
|
q: other.q,
|
||||||
vector: other.vector.map(CS::into_inner),
|
vector: other.vector.map(CS::into_inner),
|
||||||
@ -230,6 +251,7 @@ impl TryFrom<SearchQueryGet> for SearchQuery {
|
|||||||
hybrid,
|
hybrid,
|
||||||
ranking_score_threshold: other.ranking_score_threshold.map(|o| o.0),
|
ranking_score_threshold: other.ranking_score_threshold.map(|o| o.0),
|
||||||
locales: other.locales.map(|o| o.into_iter().collect()),
|
locales: other.locales.map(|o| o.into_iter().collect()),
|
||||||
|
personalization,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -125,6 +125,7 @@ impl<Method: AggregateMethod> SearchAggregator<Method> {
|
|||||||
hybrid,
|
hybrid,
|
||||||
ranking_score_threshold,
|
ranking_score_threshold,
|
||||||
locales,
|
locales,
|
||||||
|
personalization: _,
|
||||||
} = query;
|
} = query;
|
||||||
|
|
||||||
let mut ret = Self::default();
|
let mut ret = Self::default();
|
||||||
|
@ -66,6 +66,7 @@ impl MultiSearchAggregator {
|
|||||||
hybrid: _,
|
hybrid: _,
|
||||||
ranking_score_threshold: _,
|
ranking_score_threshold: _,
|
||||||
locales: _,
|
locales: _,
|
||||||
|
personalization: _,
|
||||||
} in &federated_search.queries
|
} in &federated_search.queries
|
||||||
{
|
{
|
||||||
if let Some(federation_options) = federation_options {
|
if let Some(federation_options) = federation_options {
|
||||||
|
@ -57,6 +57,15 @@ pub const DEFAULT_HIGHLIGHT_PRE_TAG: fn() -> String = || "<em>".to_string();
|
|||||||
pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "</em>".to_string();
|
pub const DEFAULT_HIGHLIGHT_POST_TAG: fn() -> String = || "</em>".to_string();
|
||||||
pub const DEFAULT_SEMANTIC_RATIO: fn() -> SemanticRatio = || SemanticRatio(0.5);
|
pub const DEFAULT_SEMANTIC_RATIO: fn() -> SemanticRatio = || SemanticRatio(0.5);
|
||||||
|
|
||||||
|
#[derive(Clone, Default, PartialEq, Deserr, ToSchema, Debug)]
|
||||||
|
#[deserr(error = DeserrJsonError<InvalidSearchPersonalization>, rename_all = camelCase, deny_unknown_fields)]
|
||||||
|
pub struct Personalization {
|
||||||
|
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalizationPersonalized>)]
|
||||||
|
pub personalized: bool,
|
||||||
|
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalizationUserProfile>)]
|
||||||
|
pub user_profile: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, PartialEq, Deserr, ToSchema)]
|
#[derive(Clone, Default, PartialEq, Deserr, ToSchema)]
|
||||||
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)]
|
||||||
pub struct SearchQuery {
|
pub struct SearchQuery {
|
||||||
@ -118,6 +127,8 @@ pub struct SearchQuery {
|
|||||||
pub ranking_score_threshold: Option<RankingScoreThreshold>,
|
pub ranking_score_threshold: Option<RankingScoreThreshold>,
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSearchLocales>)]
|
#[deserr(default, error = DeserrJsonError<InvalidSearchLocales>)]
|
||||||
pub locales: Option<Vec<Locale>>,
|
pub locales: Option<Vec<Locale>>,
|
||||||
|
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalization>, default)]
|
||||||
|
pub personalization: Option<Personalization>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SearchParameters> for SearchQuery {
|
impl From<SearchParameters> for SearchQuery {
|
||||||
@ -164,6 +175,7 @@ impl From<SearchParameters> for SearchQuery {
|
|||||||
highlight_post_tag: DEFAULT_HIGHLIGHT_POST_TAG(),
|
highlight_post_tag: DEFAULT_HIGHLIGHT_POST_TAG(),
|
||||||
crop_marker: DEFAULT_CROP_MARKER(),
|
crop_marker: DEFAULT_CROP_MARKER(),
|
||||||
locales: None,
|
locales: None,
|
||||||
|
personalization: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -244,6 +256,7 @@ impl fmt::Debug for SearchQuery {
|
|||||||
attributes_to_search_on,
|
attributes_to_search_on,
|
||||||
ranking_score_threshold,
|
ranking_score_threshold,
|
||||||
locales,
|
locales,
|
||||||
|
personalization,
|
||||||
} = self;
|
} = self;
|
||||||
|
|
||||||
let mut debug = f.debug_struct("SearchQuery");
|
let mut debug = f.debug_struct("SearchQuery");
|
||||||
@ -329,6 +342,10 @@ impl fmt::Debug for SearchQuery {
|
|||||||
debug.field("locales", &locales);
|
debug.field("locales", &locales);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(personalization) = personalization {
|
||||||
|
debug.field("personalization", &personalization);
|
||||||
|
}
|
||||||
|
|
||||||
debug.finish()
|
debug.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -531,6 +548,9 @@ pub struct SearchQueryWithIndex {
|
|||||||
pub ranking_score_threshold: Option<RankingScoreThreshold>,
|
pub ranking_score_threshold: Option<RankingScoreThreshold>,
|
||||||
#[deserr(default, error = DeserrJsonError<InvalidSearchLocales>, default)]
|
#[deserr(default, error = DeserrJsonError<InvalidSearchLocales>, default)]
|
||||||
pub locales: Option<Vec<Locale>>,
|
pub locales: Option<Vec<Locale>>,
|
||||||
|
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalization>, default)]
|
||||||
|
#[serde(skip)]
|
||||||
|
pub personalization: Option<Personalization>,
|
||||||
|
|
||||||
#[deserr(default)]
|
#[deserr(default)]
|
||||||
pub federation_options: Option<FederationOptions>,
|
pub federation_options: Option<FederationOptions>,
|
||||||
@ -587,6 +607,7 @@ impl SearchQueryWithIndex {
|
|||||||
attributes_to_search_on,
|
attributes_to_search_on,
|
||||||
ranking_score_threshold,
|
ranking_score_threshold,
|
||||||
locales,
|
locales,
|
||||||
|
personalization,
|
||||||
} = query;
|
} = query;
|
||||||
|
|
||||||
SearchQueryWithIndex {
|
SearchQueryWithIndex {
|
||||||
@ -617,6 +638,7 @@ impl SearchQueryWithIndex {
|
|||||||
attributes_to_search_on,
|
attributes_to_search_on,
|
||||||
ranking_score_threshold,
|
ranking_score_threshold,
|
||||||
locales,
|
locales,
|
||||||
|
personalization,
|
||||||
federation_options,
|
federation_options,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -651,6 +673,7 @@ impl SearchQueryWithIndex {
|
|||||||
hybrid,
|
hybrid,
|
||||||
ranking_score_threshold,
|
ranking_score_threshold,
|
||||||
locales,
|
locales,
|
||||||
|
personalization,
|
||||||
} = self;
|
} = self;
|
||||||
(
|
(
|
||||||
index_uid,
|
index_uid,
|
||||||
@ -681,6 +704,7 @@ impl SearchQueryWithIndex {
|
|||||||
hybrid,
|
hybrid,
|
||||||
ranking_score_threshold,
|
ranking_score_threshold,
|
||||||
locales,
|
locales,
|
||||||
|
personalization,
|
||||||
// do not use ..Default::default() here,
|
// do not use ..Default::default() here,
|
||||||
// rather add any missing field from `SearchQuery` to `SearchQueryWithIndex`
|
// rather add any missing field from `SearchQuery` to `SearchQueryWithIndex`
|
||||||
},
|
},
|
||||||
@ -905,7 +929,7 @@ pub struct SearchResultWithIndex {
|
|||||||
pub result: SearchResult,
|
pub result: SearchResult,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, ToSchema)]
|
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, ToSchema)]
|
||||||
#[serde(untagged)]
|
#[serde(untagged)]
|
||||||
pub enum HitsInfo {
|
pub enum HitsInfo {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
@ -1133,6 +1157,7 @@ pub fn perform_search(
|
|||||||
attributes_to_search_on: _,
|
attributes_to_search_on: _,
|
||||||
filter: _,
|
filter: _,
|
||||||
distinct: _,
|
distinct: _,
|
||||||
|
personalization: _,
|
||||||
} = query;
|
} = query;
|
||||||
|
|
||||||
let format = AttributesFormat {
|
let format = AttributesFormat {
|
||||||
|
Reference in New Issue
Block a user