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 34f18ad3a8
commit bae231fb91
7 changed files with 56 additions and 92 deletions

View File

@ -310,9 +310,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 ;
@ -653,21 +652,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.")
}
}

View File

@ -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<SearchResult, ResponseError> {
// 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<String> = 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

View File

@ -343,7 +343,7 @@ impl From<FacetSearchQuery> for SearchQuery {
hybrid,
ranking_score_threshold,
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::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<InvalidSearchLocales>)]
#[param(value_type = Vec<Locale>, explode = false)]
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>,
#[deserr(default, error = DeserrQueryParamError<InvalidSearchPersonalizeUserContext>)]
pub personalize_user_context: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, deserr::Deserr)]
@ -208,21 +205,9 @@ 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,
));
}
};
let personalize = other
.personalize_user_context
.map(|user_context| Personalize { user_context: Some(user_context) });
Ok(Self {
q: other.q,
@ -253,7 +238,7 @@ impl TryFrom<SearchQueryGet> 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,
})
}
}
@ -367,7 +352,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?;
@ -392,13 +377,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))
@ -488,7 +469,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?;
@ -516,13 +497,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))

View File

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

View File

@ -67,7 +67,7 @@ impl MultiSearchAggregator {
hybrid: _,
ranking_score_threshold: _,
locales: _,
personalization: _,
personalize: _,
} in &federated_search.queries
{
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);
#[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>,
#[deserr(error = DeserrJsonError<InvalidSearchPersonalize>, rename_all = camelCase, deny_unknown_fields)]
pub struct Personalize {
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalizeUserContext>)]
pub user_context: Option<String>,
}
#[derive(Clone, Default, PartialEq, Deserr, ToSchema)]
@ -129,8 +127,8 @@ pub struct SearchQuery {
pub ranking_score_threshold: Option<RankingScoreThreshold>,
#[deserr(default, error = DeserrJsonError<InvalidSearchLocales>)]
pub locales: Option<Vec<Locale>>,
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalization>, default)]
pub personalization: Option<Personalization>,
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalize>, default)]
pub personalize: Option<Personalize>,
}
impl From<SearchParameters> for SearchQuery {
@ -178,7 +176,7 @@ impl From<SearchParameters> for SearchQuery {
highlight_post_tag: DEFAULT_HIGHLIGHT_POST_TAG(),
crop_marker: DEFAULT_CROP_MARKER(),
locales: None,
personalization: None,
personalize: None,
}
}
}
@ -260,7 +258,7 @@ impl fmt::Debug for SearchQuery {
attributes_to_search_on,
ranking_score_threshold,
locales,
personalization,
personalize,
} = self;
let mut debug = f.debug_struct("SearchQuery");
@ -349,8 +347,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()
@ -558,9 +556,9 @@ pub struct SearchQueryWithIndex {
pub ranking_score_threshold: Option<RankingScoreThreshold>,
#[deserr(default, error = DeserrJsonError<InvalidSearchLocales>, default)]
pub locales: Option<Vec<Locale>>,
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalization>, default)]
#[deserr(default, error = DeserrJsonError<InvalidSearchPersonalize>, default)]
#[serde(skip)]
pub personalization: Option<Personalization>,
pub personalize: Option<Personalize>,
#[deserr(default)]
pub federation_options: Option<FederationOptions>,
@ -618,7 +616,7 @@ impl SearchQueryWithIndex {
attributes_to_search_on,
ranking_score_threshold,
locales,
personalization,
personalize,
} = query;
SearchQueryWithIndex {
@ -650,7 +648,7 @@ impl SearchQueryWithIndex {
attributes_to_search_on,
ranking_score_threshold,
locales,
personalization,
personalize,
federation_options,
}
}
@ -686,7 +684,7 @@ impl SearchQueryWithIndex {
hybrid,
ranking_score_threshold,
locales,
personalization,
personalize,
} = self;
(
index_uid,
@ -718,7 +716,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`
},
@ -1190,7 +1188,7 @@ pub fn perform_search(
attributes_to_search_on: _,
filter: _,
distinct: _,
personalization: _,
personalize: _,
} = query;
let format = AttributesFormat {