diff --git a/crates/meilisearch-types/src/error.rs b/crates/meilisearch-types/src/error.rs index 86f13add0..34087b96e 100644 --- a/crates/meilisearch-types/src/error.rs +++ b/crates/meilisearch-types/src/error.rs @@ -308,9 +308,8 @@ 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 ; +InvalidSearchPersonalize , InvalidRequest , BAD_REQUEST ; +InvalidSearchPersonalizeUserContext , InvalidRequest , BAD_REQUEST ; InvalidSearchMediaAndVector , InvalidRequest , BAD_REQUEST ; InvalidSettingsDisplayedAttributes , InvalidRequest , BAD_REQUEST ; InvalidSettingsDistinctAttribute , InvalidRequest , BAD_REQUEST ; @@ -640,21 +639,15 @@ impl fmt::Display for deserr_codes::InvalidNetworkSearchApiKey { } } -impl fmt::Display for deserr_codes::InvalidSearchPersonalization { +impl fmt::Display for deserr_codes::InvalidSearchPersonalize { 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.") + write!(f, "the value of `personalize` is invalid, expected a JSON object with optional `userContext` string.") } } -impl fmt::Display for deserr_codes::InvalidSearchPersonalizationPersonalized { +impl fmt::Display for deserr_codes::InvalidSearchPersonalizeUserContext { 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.") + write!(f, "the value of `userContext` is invalid, expected a string.") } } diff --git a/crates/meilisearch/src/personalization/mod.rs b/crates/meilisearch/src/personalization/mod.rs index 893aa8be4..50ec5dcad 100644 --- a/crates/meilisearch/src/personalization/mod.rs +++ b/crates/meilisearch/src/personalization/mod.rs @@ -1,4 +1,4 @@ -use crate::search::{Personalization, SearchResult}; +use crate::search::{Personalize, SearchResult}; use cohere_rust::{ api::rerank::{ReRankModel, ReRankRequest}, Cohere, @@ -26,15 +26,13 @@ impl PersonalizationService { pub async fn rerank_search_results( &self, search_result: SearchResult, - personalization: &Personalization, - query: &str, + personalize: Option<&Personalize>, + query: Option<&str>, ) -> Result { - // If personalization is not enabled or no API key, return original results - if !personalization.personalized || self.cohere.is_none() { - return Ok(search_result); - } - - let cohere = self.cohere.as_ref().unwrap(); + // If personalization is not requested, no API key, or no query, return original results + let Some(_personalize) = personalize else { return Ok(search_result) }; + let Some(cohere) = &self.cohere else { return Ok(search_result) }; + let Some(query) = query else { return Ok(search_result) }; // Extract documents for reranking let documents: Vec = search_result @@ -95,8 +93,7 @@ mod tests { #[tokio::test] async fn test_personalization_service_without_api_key() { let service = PersonalizationService::new(None); - let personalization = - Personalization { personalized: true, user_profile: Some("test user".to_string()) }; + let personalize = Personalize { user_context: Some("test user".to_string()) }; let search_result = SearchResult { hits: vec![SearchHit { @@ -116,8 +113,9 @@ mod tests { used_negative_operator: false, }; - let result = - service.rerank_search_results(search_result.clone(), &personalization, "test").await; + let result = service + .rerank_search_results(search_result.clone(), Some(&personalize), Some("test")) + .await; assert!(result.is_ok()); // Should return original results when no API key is provided @@ -128,10 +126,7 @@ mod tests { #[tokio::test] async fn test_personalization_service_disabled() { let service = PersonalizationService::new(Some("fake_key".to_string())); - let personalization = Personalization { - personalized: false, // Personalization disabled - user_profile: Some("test user".to_string()), - }; + let personalize = Personalize { user_context: Some("test user".to_string()) }; let search_result = SearchResult { hits: vec![SearchHit { @@ -151,8 +146,9 @@ mod tests { used_negative_operator: false, }; - let result = - service.rerank_search_results(search_result.clone(), &personalization, "test").await; + let result = service + .rerank_search_results(search_result.clone(), Some(&personalize), Some("test")) + .await; assert!(result.is_ok()); // Should return original results when personalization is disabled diff --git a/crates/meilisearch/src/routes/indexes/facet_search.rs b/crates/meilisearch/src/routes/indexes/facet_search.rs index 84c8dd2b9..fc0465e48 100644 --- a/crates/meilisearch/src/routes/indexes/facet_search.rs +++ b/crates/meilisearch/src/routes/indexes/facet_search.rs @@ -337,7 +337,7 @@ impl From for SearchQuery { hybrid, ranking_score_threshold, locales, - personalization: None, + personalize: None, } } } diff --git a/crates/meilisearch/src/routes/indexes/search.rs b/crates/meilisearch/src/routes/indexes/search.rs index 152e2ad2d..f8d2c6d24 100644 --- a/crates/meilisearch/src/routes/indexes/search.rs +++ b/crates/meilisearch/src/routes/indexes/search.rs @@ -22,7 +22,7 @@ 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, Personalization, + add_search_rules, perform_search, HybridQuery, MatchingStrategy, Personalize, 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, @@ -132,11 +132,8 @@ 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, + #[deserr(default, error = DeserrQueryParamError)] + pub personalize_user_context: Option, } #[derive(Debug, Clone, Copy, PartialEq, deserr::Deserr)] @@ -208,21 +205,9 @@ 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, - )); - } - }; + let personalize = other + .personalize_user_context + .map(|user_context| Personalize { user_context: Some(user_context) }); Ok(Self { q: other.q, @@ -251,7 +236,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, + personalize, }) } } @@ -365,7 +350,7 @@ pub async fn search_with_url_query( let retrieve_vector = RetrieveVectors::new(query.retrieve_vectors); // Extract personalization and query string before moving query - let personalization = query.personalization.clone(); + let personalize = query.personalize.clone(); let query_str = query.q.clone(); let permit = search_queue.try_get_search_permit().await?; @@ -390,13 +375,9 @@ pub async fn search_with_url_query( let mut search_result = search_result?; // Apply personalization if requested - if let Some(personalization) = &personalization { - if let Some(query_str) = &query_str { - search_result = personalization_service - .rerank_search_results(search_result, personalization, query_str) - .await?; - } - } + search_result = personalization_service + .rerank_search_results(search_result, personalize.as_ref(), query_str.as_deref()) + .await?; debug!(returns = ?search_result, "Search get"); Ok(HttpResponse::Ok().json(search_result)) @@ -486,7 +467,7 @@ pub async fn search_with_post( let retrieve_vectors = RetrieveVectors::new(query.retrieve_vectors); // Extract personalization and query string before moving query - let personalization = query.personalization.clone(); + let personalize = query.personalize.clone(); let query_str = query.q.clone(); let permit = search_queue.try_get_search_permit().await?; @@ -514,13 +495,9 @@ pub async fn search_with_post( let mut search_result = search_result?; // Apply personalization if requested - if let Some(personalization) = &personalization { - if let Some(query_str) = &query_str { - search_result = personalization_service - .rerank_search_results(search_result, personalization, query_str) - .await?; - } - } + search_result = personalization_service + .rerank_search_results(search_result, personalize.as_ref(), query_str.as_deref()) + .await?; debug!(returns = ?search_result, "Search post"); Ok(HttpResponse::Ok().json(search_result)) diff --git a/crates/meilisearch/src/routes/indexes/search_analytics.rs b/crates/meilisearch/src/routes/indexes/search_analytics.rs index e92b5160a..28075da2c 100644 --- a/crates/meilisearch/src/routes/indexes/search_analytics.rs +++ b/crates/meilisearch/src/routes/indexes/search_analytics.rs @@ -125,7 +125,7 @@ impl SearchAggregator { hybrid, ranking_score_threshold, locales, - personalization: _, + personalize: _, } = 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 96b5323b3..711235c51 100644 --- a/crates/meilisearch/src/routes/multi_search_analytics.rs +++ b/crates/meilisearch/src/routes/multi_search_analytics.rs @@ -66,7 +66,7 @@ impl MultiSearchAggregator { hybrid: _, ranking_score_threshold: _, locales: _, - personalization: _, + personalize: _, } 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 bf7e6c281..021a95a8d 100644 --- a/crates/meilisearch/src/search/mod.rs +++ b/crates/meilisearch/src/search/mod.rs @@ -58,12 +58,10 @@ 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, +#[deserr(error = DeserrJsonError, rename_all = camelCase, deny_unknown_fields)] +pub struct Personalize { + #[deserr(default, error = DeserrJsonError)] + pub user_context: Option, } #[derive(Clone, Default, PartialEq, Deserr, ToSchema)] @@ -127,8 +125,8 @@ pub struct SearchQuery { pub ranking_score_threshold: Option, #[deserr(default, error = DeserrJsonError)] pub locales: Option>, - #[deserr(default, error = DeserrJsonError, default)] - pub personalization: Option, + #[deserr(default, error = DeserrJsonError, default)] + pub personalize: Option, } impl From for SearchQuery { @@ -175,7 +173,7 @@ impl From for SearchQuery { highlight_post_tag: DEFAULT_HIGHLIGHT_POST_TAG(), crop_marker: DEFAULT_CROP_MARKER(), locales: None, - personalization: None, + personalize: None, } } } @@ -256,7 +254,7 @@ impl fmt::Debug for SearchQuery { attributes_to_search_on, ranking_score_threshold, locales, - personalization, + personalize, } = self; let mut debug = f.debug_struct("SearchQuery"); @@ -342,8 +340,8 @@ impl fmt::Debug for SearchQuery { debug.field("locales", &locales); } - if let Some(personalization) = personalization { - debug.field("personalization", &personalization); + if let Some(personalize) = personalize { + debug.field("personalize", &personalize); } debug.finish() @@ -548,9 +546,9 @@ pub struct SearchQueryWithIndex { pub ranking_score_threshold: Option, #[deserr(default, error = DeserrJsonError, default)] pub locales: Option>, - #[deserr(default, error = DeserrJsonError, default)] + #[deserr(default, error = DeserrJsonError, default)] #[serde(skip)] - pub personalization: Option, + pub personalize: Option, #[deserr(default)] pub federation_options: Option, @@ -607,7 +605,7 @@ impl SearchQueryWithIndex { attributes_to_search_on, ranking_score_threshold, locales, - personalization, + personalize, } = query; SearchQueryWithIndex { @@ -638,7 +636,7 @@ impl SearchQueryWithIndex { attributes_to_search_on, ranking_score_threshold, locales, - personalization, + personalize, federation_options, } } @@ -673,7 +671,7 @@ impl SearchQueryWithIndex { hybrid, ranking_score_threshold, locales, - personalization, + personalize, } = self; ( index_uid, @@ -704,7 +702,7 @@ impl SearchQueryWithIndex { hybrid, ranking_score_threshold, locales, - personalization, + personalize, // do not use ..Default::default() here, // rather add any missing field from `SearchQuery` to `SearchQueryWithIndex` }, @@ -1157,7 +1155,7 @@ pub fn perform_search( attributes_to_search_on: _, filter: _, distinct: _, - personalization: _, + personalize: _, } = query; let format = AttributesFormat {