refactor: rename personalization API fields and move checks inside service

- Rename 'personalization' field to 'personalize' in API
- Rename 'userProfile' to 'userContext' in personalization object
- Remove 'personalized' boolean field (activation now based on non-null 'personalize')
- Move personalization checks inside rerank_search_results function
- Use 'let else' pattern for better error handling
- Update error types and messages to reflect new field names
- Update all search routes and analytics to use new field names
This commit is contained in:
ManyTheFish
2025-07-23 09:48:01 +02:00
parent ed17e0cb21
commit 07ecf63c22
7 changed files with 56 additions and 92 deletions

View File

@ -308,9 +308,8 @@ 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 ; InvalidSearchPersonalize , InvalidRequest , BAD_REQUEST ;
InvalidSearchPersonalizationPersonalized , InvalidRequest , BAD_REQUEST ; InvalidSearchPersonalizeUserContext , InvalidRequest , BAD_REQUEST ;
InvalidSearchPersonalizationUserProfile , InvalidRequest , BAD_REQUEST ;
InvalidSearchMediaAndVector , InvalidRequest , BAD_REQUEST ; InvalidSearchMediaAndVector , InvalidRequest , BAD_REQUEST ;
InvalidSettingsDisplayedAttributes , InvalidRequest , BAD_REQUEST ; InvalidSettingsDisplayedAttributes , InvalidRequest , BAD_REQUEST ;
InvalidSettingsDistinctAttribute , 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 { 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 { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "the value of `personalized` is invalid, expected a boolean.") write!(f, "the value of `userContext` is invalid, expected a string.")
}
}
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.")
} }
} }

View File

@ -1,4 +1,4 @@
use crate::search::{Personalization, SearchResult}; use crate::search::{Personalize, SearchResult};
use cohere_rust::{ use cohere_rust::{
api::rerank::{ReRankModel, ReRankRequest}, api::rerank::{ReRankModel, ReRankRequest},
Cohere, Cohere,
@ -26,15 +26,13 @@ impl PersonalizationService {
pub async fn rerank_search_results( pub async fn rerank_search_results(
&self, &self,
search_result: SearchResult, search_result: SearchResult,
personalization: &Personalization, personalize: Option<&Personalize>,
query: &str, query: Option<&str>,
) -> Result<SearchResult, ResponseError> { ) -> Result<SearchResult, ResponseError> {
// If personalization is not enabled or no API key, return original results // If personalization is not requested, no API key, or no query, return original results
if !personalization.personalized || self.cohere.is_none() { let Some(_personalize) = personalize else { return Ok(search_result) };
return Ok(search_result); let Some(cohere) = &self.cohere else { return Ok(search_result) };
} let Some(query) = query else { return Ok(search_result) };
let cohere = self.cohere.as_ref().unwrap();
// Extract documents for reranking // Extract documents for reranking
let documents: Vec<String> = search_result let documents: Vec<String> = search_result
@ -95,8 +93,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_personalization_service_without_api_key() { async fn test_personalization_service_without_api_key() {
let service = PersonalizationService::new(None); let service = PersonalizationService::new(None);
let personalization = let personalize = Personalize { user_context: Some("test user".to_string()) };
Personalization { personalized: true, user_profile: Some("test user".to_string()) };
let search_result = SearchResult { let search_result = SearchResult {
hits: vec![SearchHit { hits: vec![SearchHit {
@ -116,8 +113,9 @@ mod tests {
used_negative_operator: false, used_negative_operator: false,
}; };
let result = let result = service
service.rerank_search_results(search_result.clone(), &personalization, "test").await; .rerank_search_results(search_result.clone(), Some(&personalize), Some("test"))
.await;
assert!(result.is_ok()); assert!(result.is_ok());
// Should return original results when no API key is provided // Should return original results when no API key is provided
@ -128,10 +126,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_personalization_service_disabled() { async fn test_personalization_service_disabled() {
let service = PersonalizationService::new(Some("fake_key".to_string())); let service = PersonalizationService::new(Some("fake_key".to_string()));
let personalization = Personalization { let personalize = Personalize { user_context: Some("test user".to_string()) };
personalized: false, // Personalization disabled
user_profile: Some("test user".to_string()),
};
let search_result = SearchResult { let search_result = SearchResult {
hits: vec![SearchHit { hits: vec![SearchHit {
@ -151,8 +146,9 @@ mod tests {
used_negative_operator: false, used_negative_operator: false,
}; };
let result = let result = service
service.rerank_search_results(search_result.clone(), &personalization, "test").await; .rerank_search_results(search_result.clone(), Some(&personalize), Some("test"))
.await;
assert!(result.is_ok()); assert!(result.is_ok());
// Should return original results when personalization is disabled // Should return original results when personalization is disabled

View File

@ -337,7 +337,7 @@ impl From<FacetSearchQuery> for SearchQuery {
hybrid, hybrid,
ranking_score_threshold, ranking_score_threshold,
locales, locales,
personalization: None, personalize: None,
} }
} }
} }

View File

@ -22,7 +22,7 @@ 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, Personalization, add_search_rules, perform_search, HybridQuery, MatchingStrategy, Personalize,
RankingScoreThreshold, RetrieveVectors, SearchKind, SearchQuery, SearchResult, SemanticRatio, RankingScoreThreshold, RetrieveVectors, SearchKind, SearchQuery, SearchResult, SemanticRatio,
DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG, DEFAULT_CROP_LENGTH, DEFAULT_CROP_MARKER, DEFAULT_HIGHLIGHT_POST_TAG,
DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, DEFAULT_SEMANTIC_RATIO, DEFAULT_HIGHLIGHT_PRE_TAG, DEFAULT_SEARCH_LIMIT, DEFAULT_SEARCH_OFFSET, DEFAULT_SEMANTIC_RATIO,
@ -132,11 +132,8 @@ 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>)] #[deserr(default, error = DeserrQueryParamError<InvalidSearchPersonalizeUserContext>)]
#[param(value_type = bool)] pub personalize_user_context: Option<String>,
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)]
@ -208,21 +205,9 @@ impl TryFrom<SearchQueryGet> for SearchQuery {
)); ));
} }
let personalization = match ( let personalize = other
other.personalization_personalized, .personalize_user_context
other.personalization_user_profile, .map(|user_context| Personalize { user_context: Some(user_context) });
) {
(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,
@ -251,7 +236,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, personalize,
}) })
} }
} }
@ -365,7 +350,7 @@ pub async fn search_with_url_query(
let retrieve_vector = RetrieveVectors::new(query.retrieve_vectors); let retrieve_vector = RetrieveVectors::new(query.retrieve_vectors);
// Extract personalization and query string before moving query // 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 query_str = query.q.clone();
let permit = search_queue.try_get_search_permit().await?; 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?; let mut search_result = search_result?;
// Apply personalization if requested // Apply personalization if requested
if let Some(personalization) = &personalization {
if let Some(query_str) = &query_str {
search_result = personalization_service search_result = personalization_service
.rerank_search_results(search_result, personalization, query_str) .rerank_search_results(search_result, personalize.as_ref(), query_str.as_deref())
.await?; .await?;
}
}
debug!(returns = ?search_result, "Search get"); debug!(returns = ?search_result, "Search get");
Ok(HttpResponse::Ok().json(search_result)) Ok(HttpResponse::Ok().json(search_result))
@ -486,7 +467,7 @@ pub async fn search_with_post(
let retrieve_vectors = RetrieveVectors::new(query.retrieve_vectors); let retrieve_vectors = RetrieveVectors::new(query.retrieve_vectors);
// Extract personalization and query string before moving query // 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 query_str = query.q.clone();
let permit = search_queue.try_get_search_permit().await?; 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?; let mut search_result = search_result?;
// Apply personalization if requested // Apply personalization if requested
if let Some(personalization) = &personalization {
if let Some(query_str) = &query_str {
search_result = personalization_service search_result = personalization_service
.rerank_search_results(search_result, personalization, query_str) .rerank_search_results(search_result, personalize.as_ref(), query_str.as_deref())
.await?; .await?;
}
}
debug!(returns = ?search_result, "Search post"); debug!(returns = ?search_result, "Search post");
Ok(HttpResponse::Ok().json(search_result)) Ok(HttpResponse::Ok().json(search_result))

View File

@ -125,7 +125,7 @@ impl<Method: AggregateMethod> SearchAggregator<Method> {
hybrid, hybrid,
ranking_score_threshold, ranking_score_threshold,
locales, locales,
personalization: _, personalize: _,
} = query; } = query;
let mut ret = Self::default(); let mut ret = Self::default();

View File

@ -66,7 +66,7 @@ impl MultiSearchAggregator {
hybrid: _, hybrid: _,
ranking_score_threshold: _, ranking_score_threshold: _,
locales: _, locales: _,
personalization: _, personalize: _,
} in &federated_search.queries } in &federated_search.queries
{ {
if let Some(federation_options) = federation_options { if let Some(federation_options) = federation_options {

View File

@ -58,12 +58,10 @@ 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)] #[derive(Clone, Default, PartialEq, Deserr, ToSchema, Debug)]
#[deserr(error = DeserrJsonError<InvalidSearchPersonalization>, rename_all = camelCase, deny_unknown_fields)] #[deserr(error = DeserrJsonError<InvalidSearchPersonalize>, rename_all = camelCase, deny_unknown_fields)]
pub struct Personalization { pub struct Personalize {
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalizationPersonalized>)] #[deserr(default, error = DeserrJsonError<InvalidSearchPersonalizeUserContext>)]
pub personalized: bool, pub user_context: Option<String>,
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalizationUserProfile>)]
pub user_profile: Option<String>,
} }
#[derive(Clone, Default, PartialEq, Deserr, ToSchema)] #[derive(Clone, Default, PartialEq, Deserr, ToSchema)]
@ -127,8 +125,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)] #[deserr(default, error = DeserrJsonError<InvalidSearchPersonalize>, default)]
pub personalization: Option<Personalization>, pub personalize: Option<Personalize>,
} }
impl From<SearchParameters> for SearchQuery { impl From<SearchParameters> for SearchQuery {
@ -175,7 +173,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, personalize: None,
} }
} }
} }
@ -256,7 +254,7 @@ impl fmt::Debug for SearchQuery {
attributes_to_search_on, attributes_to_search_on,
ranking_score_threshold, ranking_score_threshold,
locales, locales,
personalization, personalize,
} = self; } = self;
let mut debug = f.debug_struct("SearchQuery"); let mut debug = f.debug_struct("SearchQuery");
@ -342,8 +340,8 @@ impl fmt::Debug for SearchQuery {
debug.field("locales", &locales); debug.field("locales", &locales);
} }
if let Some(personalization) = personalization { if let Some(personalize) = personalize {
debug.field("personalization", &personalization); debug.field("personalize", &personalize);
} }
debug.finish() debug.finish()
@ -548,9 +546,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)] #[deserr(default, error = DeserrJsonError<InvalidSearchPersonalize>, default)]
#[serde(skip)] #[serde(skip)]
pub personalization: Option<Personalization>, pub personalize: Option<Personalize>,
#[deserr(default)] #[deserr(default)]
pub federation_options: Option<FederationOptions>, pub federation_options: Option<FederationOptions>,
@ -607,7 +605,7 @@ impl SearchQueryWithIndex {
attributes_to_search_on, attributes_to_search_on,
ranking_score_threshold, ranking_score_threshold,
locales, locales,
personalization, personalize,
} = query; } = query;
SearchQueryWithIndex { SearchQueryWithIndex {
@ -638,7 +636,7 @@ impl SearchQueryWithIndex {
attributes_to_search_on, attributes_to_search_on,
ranking_score_threshold, ranking_score_threshold,
locales, locales,
personalization, personalize,
federation_options, federation_options,
} }
} }
@ -673,7 +671,7 @@ impl SearchQueryWithIndex {
hybrid, hybrid,
ranking_score_threshold, ranking_score_threshold,
locales, locales,
personalization, personalize,
} = self; } = self;
( (
index_uid, index_uid,
@ -704,7 +702,7 @@ impl SearchQueryWithIndex {
hybrid, hybrid,
ranking_score_threshold, ranking_score_threshold,
locales, locales,
personalization, personalize,
// 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`
}, },
@ -1157,7 +1155,7 @@ pub fn perform_search(
attributes_to_search_on: _, attributes_to_search_on: _,
filter: _, filter: _,
distinct: _, distinct: _,
personalization: _, personalize: _,
} = query; } = query;
let format = AttributesFormat { let format = AttributesFormat {