This is an automated email from the ASF dual-hosted git repository. jkevan pushed a commit to branch refactor-control-groups in repository https://gitbox.apache.org/repos/asf/unomi.git
commit 5222604ef1a7eb0b44a159f917ce96ae99345cbe Author: Kevan <ke...@jahia.com> AuthorDate: Tue Nov 8 15:26:18 2022 +0100 UNOMI-690, UNOMI-696: refactor control group --- .../java/org/apache/unomi/api/ContextResponse.java | 32 +++++- .../apache/unomi/api/PersonalizationResult.java | 65 +++++++++++- .../apache/unomi/api/PersonalizationStrategy.java | 4 +- .../main/java/org/apache/unomi/api/Profile.java | 2 +- .../main/java/org/apache/unomi/api/Session.java | 2 +- ...zationResult.java => SystemPropertiesItem.java} | 29 ++---- .../unomi/rest/endpoints/ContextJsonEndpoint.java | 5 + .../impl/personalization/ControlGroup.java | 101 ------------------ .../PersonalizationServiceImpl.java | 88 ++++------------ .../sorts/ControlGroupPersonalizationStrategy.java | 115 +++++++++++++++++++++ .../sorts/FilterPersonalizationStrategy.java | 5 +- .../sorts/RandomPersonalizationStrategy.java | 8 +- .../sorts/ScorePersonalizationStrategy.java | 15 ++- 13 files changed, 253 insertions(+), 218 deletions(-) diff --git a/api/src/main/java/org/apache/unomi/api/ContextResponse.java b/api/src/main/java/org/apache/unomi/api/ContextResponse.java index b7467e018..cb0ee4ab8 100644 --- a/api/src/main/java/org/apache/unomi/api/ContextResponse.java +++ b/api/src/main/java/org/apache/unomi/api/ContextResponse.java @@ -21,10 +21,7 @@ import org.apache.unomi.api.conditions.Condition; import org.apache.unomi.api.services.RulesService; import java.io.Serializable; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; /** * A context server response resulting from the evaluation of a client's context request. Note that all returned values result of the evaluation of the data provided in the @@ -60,6 +57,8 @@ public class ContextResponse implements Serializable { private Map<String, Consent> consents = new LinkedHashMap<>(); + private Map<String, Object> additionalResponseData = new HashMap<>(); + /** * Retrieves the profile identifier associated with the profile of the user on behalf of which the client performed the context request. * @@ -198,10 +197,20 @@ public class ContextResponse implements Serializable { this.processedEvents = processedEvents; } + /** + * @deprecated personalizations results are more complex since 2.1.0 and they are now available under: + * additionalResponseData.personalizationResults + */ + @Deprecated public Map<String, List<String>> getPersonalizations() { return personalizations; } + /** + * @deprecated personalizations results are more complex since 2.1.0 and they are now available under: + * additionalResponseData.personalizationResults + */ + @Deprecated public void setPersonalizations(Map<String, List<String>> personalizations) { this.personalizations = personalizations; } @@ -266,4 +275,19 @@ public class ContextResponse implements Serializable { public void setConsents(Map<String, Consent> consents) { this.consents = consents; } + + /** + * Useful open map for additional information that could be returned by Unomi during the context request. + * @return additional properties + */ + public Map<String, Object> getAdditionalResponseData() { + return additionalResponseData; + } + + /** + * Useful open map for additional information that could be returned by Unomi during the context request. + */ + public void setAdditionalResponseData(Map<String, Object> additionalResponseData) { + this.additionalResponseData = additionalResponseData; + } } diff --git a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java b/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java index 446189a9a..ceb2e2ade 100644 --- a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java +++ b/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java @@ -16,27 +16,82 @@ */ package org.apache.unomi.api; +import org.apache.unomi.api.services.EventService; + +import javax.xml.bind.annotation.XmlTransient; +import java.io.Serializable; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * A class to contain the result of a personalization, containing the list of content IDs as well as a changeType to - * indicate if a profile and/or a session was modified (to store control group information). + * indicate if a profile and/or a session was modified. */ -public class PersonalizationResult { +public class PersonalizationResult implements Serializable { + + public final static String ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP = "inControlGroup"; List<String> contentIds; - int changeType; - public PersonalizationResult(List<String> contentIds, int changeType) { + Map<String, Object> additionalResultInfos = new HashMap<>(); + + int changeType = EventService.NO_CHANGE; + + public PersonalizationResult(List<String> contentIds) { this.contentIds = contentIds; - this.changeType = changeType; } + /** + * List of matching ids for current personalization + * @return the list of matching ids + */ public List<String> getContentIds() { return contentIds; } + /** + * Useful open map to return additional result information to the client + * @return map of key/value pair for additional information, like: inControlGroup + */ + public Map<String, Object> getAdditionalResultInfos() { + return additionalResultInfos; + } + + public void setAdditionalResultInfos(Map<String, Object> additionalResultInfos) { + this.additionalResultInfos = additionalResultInfos; + } + + /** + * Is the current personalization result in a control group ? + * Control group are used to identify a profile or a session that should not get personalized results, + * instead the current profile/session should get a specific result (usually the same for all peoples falling in control group) + * Note: it's for now the responsibility of the client to decide what to do when the current personalization is under control group. + * + * @return true in case current profile or session is in control group for the personalization. + */ + @XmlTransient + public boolean isInControlGroup() { + return additionalResultInfos.containsKey(ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP) && + (Boolean) additionalResultInfos.get(ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP); + } + + public void setInControlGroup(boolean inControlGroup) { + this.additionalResultInfos.put(ADDITIONAL_RESULT_INFO_IN_CONTROL_GROUP, inControlGroup); + } + + /** + * Change code in case the personalization resolution modified the profile or the session + * Only used internally, and will not be serialized either for storage or response payload. + * + * @return change code + */ + @XmlTransient public int getChangeType() { return changeType; } + + public void addChanges(int changes) { + this.changeType |= changes; + } } diff --git a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java b/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java index 152625e3f..5ceb941f7 100644 --- a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java +++ b/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java @@ -32,7 +32,7 @@ public interface PersonalizationStrategy { * @param session the session to use for the personalization * @param personalizationRequest the request contains the contents to personalizes as well as the parameters for the * strategy (options) - * @return a list of content IDs resulting from the filtering/re-ordering + * @return the personalization result that contains the list of content IDs resulting from the filtering/re-ordering */ - List<String> personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest); + PersonalizationResult personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest); } diff --git a/api/src/main/java/org/apache/unomi/api/Profile.java b/api/src/main/java/org/apache/unomi/api/Profile.java index ea9583850..133fc7599 100644 --- a/api/src/main/java/org/apache/unomi/api/Profile.java +++ b/api/src/main/java/org/apache/unomi/api/Profile.java @@ -38,7 +38,7 @@ import java.util.*; * * @see Segment */ -public class Profile extends Item { +public class Profile extends Item implements SystemPropertiesItem { /** * The Profile ITEM_TYPE diff --git a/api/src/main/java/org/apache/unomi/api/Session.java b/api/src/main/java/org/apache/unomi/api/Session.java index 986323c12..8ca067720 100644 --- a/api/src/main/java/org/apache/unomi/api/Session.java +++ b/api/src/main/java/org/apache/unomi/api/Session.java @@ -27,7 +27,7 @@ import java.util.Map; * A time-bounded interaction between a user (via their associated {@link Profile}) and a unomi-enabled application. A session represents a sequence of operations the user * performed during its duration. In the context of web applications, sessions are usually linked to HTTP sessions. */ -public class Session extends Item implements TimestampedItem { +public class Session extends Item implements TimestampedItem, SystemPropertiesItem { /** * The Session ITEM_TYPE. diff --git a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java b/api/src/main/java/org/apache/unomi/api/SystemPropertiesItem.java similarity index 57% copy from api/src/main/java/org/apache/unomi/api/PersonalizationResult.java copy to api/src/main/java/org/apache/unomi/api/SystemPropertiesItem.java index 446189a9a..e191562cc 100644 --- a/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java +++ b/api/src/main/java/org/apache/unomi/api/SystemPropertiesItem.java @@ -16,27 +16,18 @@ */ package org.apache.unomi.api; -import java.util.List; +import java.util.Map; /** - * A class to contain the result of a personalization, containing the list of content IDs as well as a changeType to - * indicate if a profile and/or a session was modified (to store control group information). + * An Item that is holding system properties. */ -public class PersonalizationResult { +public interface SystemPropertiesItem { - List<String> contentIds; - int changeType; - - public PersonalizationResult(List<String> contentIds, int changeType) { - this.contentIds = contentIds; - this.changeType = changeType; - } - - public List<String> getContentIds() { - return contentIds; - } - - public int getChangeType() { - return changeType; - } + /** + * Retrieves a Map of system property name - value pairs for this item. System properties can be used by implementations to store non-user visible properties needed for + * internal purposes. + * + * @return a Map of system property name - value pairs for this item + */ + Map<String, Object> getSystemProperties(); } diff --git a/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java b/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java index 6207845f8..49b1c535b 100644 --- a/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java +++ b/rest/src/main/java/org/apache/unomi/rest/endpoints/ContextJsonEndpoint.java @@ -204,11 +204,16 @@ public class ContextJsonEndpoint { List<PersonalizationService.PersonalizationRequest> personalizations = contextRequest.getPersonalizations(); if (personalizations != null) { data.setPersonalizations(new HashMap<>()); + Map<String, PersonalizationResult> personalizationResults = new HashMap<>(); for (PersonalizationService.PersonalizationRequest personalization : sanitizePersonalizations(personalizations)) { PersonalizationResult personalizationResult = personalizationService.personalizeList(eventsRequestContext.getProfile(), eventsRequestContext.getSession(), personalization); eventsRequestContext.addChanges(personalizationResult.getChangeType()); + + // Support for old personalization result in response data.getPersonalizations().put(personalization.getId(), personalizationResult.getContentIds()); + personalizationResults.put(personalization.getId(), personalizationResult); } + data.getAdditionalResponseData().put("personalizationResults", personalizationResults); } if (contextRequest.isRequireSegments()) { diff --git a/services/src/main/java/org/apache/unomi/services/impl/personalization/ControlGroup.java b/services/src/main/java/org/apache/unomi/services/impl/personalization/ControlGroup.java deleted file mode 100644 index 795080e87..000000000 --- a/services/src/main/java/org/apache/unomi/services/impl/personalization/ControlGroup.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.unomi.services.impl.personalization; - -import org.apache.unomi.persistence.spi.CustomObjectMapper; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.text.ParseException; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.Map; - -/** - * Represents a personalization control group, stored in a profile and/or a session - */ -public class ControlGroup { - - private static final Logger logger = LoggerFactory.getLogger(ControlGroup.class.getName()); - - String id; - String displayName; - String path; - Date timeStamp; - - public ControlGroup(String id, String displayName, String path, Date timeStamp) { - this.id = id; - this.displayName = displayName; - this.path = path; - this.timeStamp = timeStamp; - } - - public static ControlGroup fromMap(Map<String,Object> map) { - String id = (String) map.get("id"); - String displayName = (String) map.get("displayName"); - String path = (String) map.get("path"); - String dateStr = (String) map.get("timeStamp"); - Date date = null; - try { - date = CustomObjectMapper.getObjectMapper().getDateFormat().parse(dateStr); - } catch (ParseException e) { - logger.error("Error parsing control group date", e); - } - return new ControlGroup(id, displayName, path, date); - } - - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public String getDisplayName() { - return displayName; - } - - public void setDisplayName(String displayName) { - this.displayName = displayName; - } - - public String getPath() { - return path; - } - - public void setPath(String path) { - this.path = path; - } - - public Date getTimeStamp() { - return timeStamp; - } - - public void setTimeStamp(Date timeStamp) { - this.timeStamp = timeStamp; - } - - public Map<String,Object> toMap() { - Map<String,Object> result = new LinkedHashMap<>(); - result.put("id", id); - result.put("displayName", displayName); - result.put("path", path); - result.put("timeStamp", CustomObjectMapper.getObjectMapper().getDateFormat().format(timeStamp)); - return result; - } -} diff --git a/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java b/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java index 1d2cfd760..9fef8ba2c 100644 --- a/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java +++ b/services/src/main/java/org/apache/unomi/services/impl/personalization/PersonalizationServiceImpl.java @@ -17,30 +17,27 @@ package org.apache.unomi.services.impl.personalization; -import org.apache.unomi.api.PersonalizationResult; -import org.apache.unomi.api.PersonalizationStrategy; -import org.apache.unomi.api.Profile; -import org.apache.unomi.api.Session; +import org.apache.unomi.api.*; import org.apache.unomi.api.conditions.Condition; -import org.apache.unomi.api.services.EventService; import org.apache.unomi.api.services.PersonalizationService; import org.apache.unomi.api.services.ProfileService; +import org.apache.unomi.services.sorts.ControlGroupPersonalizationStrategy; import org.osgi.framework.BundleContext; import org.osgi.framework.ServiceReference; import java.util.*; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; + +import static org.apache.unomi.services.sorts.ControlGroupPersonalizationStrategy.CONTROL_GROUP_CONFIG; public class PersonalizationServiceImpl implements PersonalizationService { - public static final String CONTROL_GROUPS_PROPERTY_NAME = "unomiControlGroups"; private BundleContext bundleContext; private ProfileService profileService; private Map<String, PersonalizationStrategy> personalizationStrategies = new ConcurrentHashMap<>(); + private final PersonalizationStrategy controlGroupStrategy = new ControlGroupPersonalizationStrategy(); - private Random controlGroupRandom = new Random(); public void setProfileService(ProfileService profileService) { this.profileService = profileService; @@ -88,73 +85,24 @@ public class PersonalizationServiceImpl implements PersonalizationService { @Override public PersonalizationResult personalizeList(Profile profile, Session session, PersonalizationRequest personalizationRequest) { PersonalizationStrategy strategy = personalizationStrategies.get(personalizationRequest.getStrategy()); - int changeType = EventService.NO_CHANGE; if (strategy != null) { - if (personalizationRequest.getStrategyOptions() != null && personalizationRequest.getStrategyOptions().containsKey("controlGroup")) { - Map<String,Object> controlGroupMap = (Map<String,Object>) personalizationRequest.getStrategyOptions().get("controlGroup"); - - boolean storeInSession = false; - if (controlGroupMap.containsKey("storeInSession")) { - storeInSession = (Boolean) controlGroupMap.get("storeInSession"); + // hook on control group if necessary + PersonalizationResult controlGroupStrategyResult = null; + if (personalizationRequest.getStrategyOptions() != null && personalizationRequest.getStrategyOptions().containsKey(CONTROL_GROUP_CONFIG)) { + controlGroupStrategyResult = controlGroupStrategy.personalizeList(profile, session, personalizationRequest); + if (controlGroupStrategyResult.isInControlGroup()) { + return controlGroupStrategyResult; } + } - boolean profileInControlGroup = false; - Optional<ControlGroup> currentControlGroup; - - List<ControlGroup> controlGroups = null; - if (storeInSession) { - if (session.getProperty(CONTROL_GROUPS_PROPERTY_NAME) != null) { - controlGroups = ((List<Map<String, Object>>) session.getProperty(CONTROL_GROUPS_PROPERTY_NAME)).stream().map(ControlGroup::fromMap).collect(Collectors.toList()); - } - } else { - if (profile.getProperty(CONTROL_GROUPS_PROPERTY_NAME) != null) { - controlGroups = ((List<Map<String, Object>>) profile.getProperty(CONTROL_GROUPS_PROPERTY_NAME)).stream().map(ControlGroup::fromMap).collect(Collectors.toList()); - } - } - if (controlGroups == null) { - controlGroups = new ArrayList<>(); - } - currentControlGroup = controlGroups.stream().filter(controlGroup -> controlGroup.id.equals(personalizationRequest.getId())).findFirst(); - if (currentControlGroup.isPresent()) { - // we already have an entry for this personalization so this means the profile is in the control group - profileInControlGroup = true; - } else { - double randomDouble = controlGroupRandom.nextDouble() * 100.0; - Object percentageObject = controlGroupMap.get("percentage"); - Double controlGroupPercentage = null; - if (percentageObject != null) { - if (percentageObject instanceof Double) { - controlGroupPercentage = (Double) percentageObject; - } else if (percentageObject instanceof Integer) { - controlGroupPercentage = ((Integer) percentageObject).doubleValue(); - } - } - - if (randomDouble <= controlGroupPercentage) { - // Profile is elected to be in control group - profileInControlGroup = true; - ControlGroup controlGroup = new ControlGroup(personalizationRequest.getId(), - (String) controlGroupMap.get("displayName"), - (String) controlGroupMap.get("path"), - new Date()); - controlGroups.add(controlGroup); - List<Map<String,Object>> controlGroupsMap = controlGroups.stream().map(ControlGroup::toMap).collect(Collectors.toList()); - if (storeInSession) { - session.setProperty(CONTROL_GROUPS_PROPERTY_NAME, controlGroupsMap); - changeType = EventService.SESSION_UPDATED; - } else { - profile.setProperty(CONTROL_GROUPS_PROPERTY_NAME, controlGroupsMap); - changeType = EventService.PROFILE_UPDATED; - } - } - } - if (profileInControlGroup) { - // if profile is in control group we return the unmodified list. - return new PersonalizationResult(personalizationRequest.getContents().stream().map(PersonalizedContent::getId).collect(Collectors.toList()), changeType); - } + // Execute the original strategy + PersonalizationResult originalStrategyResult = strategy.personalizeList(profile, session, personalizationRequest); + // even if control group is false, profile or session could have been modified + if (controlGroupStrategyResult != null) { + originalStrategyResult.addChanges(controlGroupStrategyResult.getChangeType()); } - return new PersonalizationResult(strategy.personalizeList(profile, session, personalizationRequest), changeType); + return originalStrategyResult; } throw new IllegalArgumentException("Unknown strategy : "+ personalizationRequest.getStrategy()); diff --git a/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java b/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java new file mode 100644 index 000000000..3361d5182 --- /dev/null +++ b/services/src/main/java/org/apache/unomi/services/sorts/ControlGroupPersonalizationStrategy.java @@ -0,0 +1,115 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.unomi.services.sorts; + +import org.apache.unomi.api.*; +import org.apache.unomi.api.services.EventService; +import org.apache.unomi.api.services.PersonalizationService; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * System strategy to calculate control group on personalization that would use such configuration + * The status is then stored in the current profile/session under: systemProperties.personalizationStrategyStatus + * A control group is used with a percentage to decide if the current profile/session should have personalized results or not. + * - in case of control group is true: the variants will be return untouched like received and an information will be added to the personalized results to warn the client + * - in case of control group is false: we will execute the personalization normally + */ +public class ControlGroupPersonalizationStrategy implements PersonalizationStrategy { + public static final String PERSONALIZATION_STRATEGY_STATUS = "personalizationStrategyStatus"; + public static final String PERSONALIZATION_STRATEGY_STATUS_ID = "personalizationId"; + public static final String PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP = "inControlGroup"; + public static final String PERSONALIZATION_STRATEGY_STATUS_DATE = "timeStamp"; + + public static final String CONTROL_GROUP_CONFIG_STORE_IN_SESSION = "storeInSession"; + public static final String CONTROL_GROUP_CONFIG_PERCENTAGE = "percentage"; + public static final String CONTROL_GROUP_CONFIG = "controlGroup"; + + private final Random controlGroupRandom = new Random(); + + @Override + public PersonalizationResult personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest) { + if (personalizationRequest.getStrategyOptions() != null && personalizationRequest.getStrategyOptions().containsKey(CONTROL_GROUP_CONFIG)) { + Map<String, Object> controlGroupMap = (Map<String, Object>) personalizationRequest.getStrategyOptions().get(CONTROL_GROUP_CONFIG); + + return controlGroupMap.containsKey(CONTROL_GROUP_CONFIG_STORE_IN_SESSION) ? + getPersonalizationResultForControlGroup(personalizationRequest, session, controlGroupMap, EventService.SESSION_UPDATED) : + getPersonalizationResultForControlGroup(personalizationRequest, profile, controlGroupMap, EventService.PROFILE_UPDATED); + } + + throw new IllegalArgumentException("Not possible to perform control group strategy without control group config"); + } + + private PersonalizationResult getPersonalizationResultForControlGroup(PersonalizationService.PersonalizationRequest personalizationRequest, SystemPropertiesItem systemPropertiesItem, Map<String, Object> controlGroupConfig, int changeType) { + // Control group will return the same untouched list of received content ids + PersonalizationResult personalizationResult = new PersonalizationResult( + personalizationRequest.getContents().stream().map(PersonalizationService.PersonalizedContent::getId).collect(Collectors.toList())); + + // get the list of existing personalization strategy status + List<Map<String, Object>> strategyStatuses; + if (systemPropertiesItem.getSystemProperties().get(PERSONALIZATION_STRATEGY_STATUS) == null) { + strategyStatuses = new ArrayList<>(); + systemPropertiesItem.getSystemProperties().put(PERSONALIZATION_STRATEGY_STATUS, strategyStatuses); + } else { + strategyStatuses = (List<Map<String, Object>>) systemPropertiesItem.getSystemProperties().get(PERSONALIZATION_STRATEGY_STATUS); + } + + // Check if we need to update an old status that would not contains control group info + boolean inControlGroup; + for (Map<String, Object> oldStrategyStatus : strategyStatuses) { + if (personalizationRequest.getId().equals(oldStrategyStatus.get(PERSONALIZATION_STRATEGY_STATUS_ID))) { + // Check if we have to update the strategy status or not ? + if (!oldStrategyStatus.containsKey(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP)) { + + // Old status doesn't contain any control group check, we need to calculate it and update the old status with the result. + inControlGroup = calculateControlGroup(controlGroupConfig); + oldStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP, inControlGroup); + oldStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_DATE, new Date()); + personalizationResult.addChanges(changeType); + + } else { + // Just read existing status about the control group + inControlGroup = (boolean) oldStrategyStatus.get(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP); + } + + personalizationResult.setInControlGroup(inControlGroup); + return personalizationResult; + } + } + + // We didn't found any existing status for the current perso, we need to create a new one. + inControlGroup = calculateControlGroup(controlGroupConfig); + Map<String, Object> newStrategyStatus = new HashMap<>(); + newStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_ID, personalizationRequest.getId()); + newStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_DATE, new Date()); + newStrategyStatus.put(PERSONALIZATION_STRATEGY_STATUS_IN_CTRL_GROUP, inControlGroup); + strategyStatuses.add(newStrategyStatus); + + personalizationResult.addChanges(changeType); + personalizationResult.setInControlGroup(inControlGroup); + return personalizationResult; + } + + private boolean calculateControlGroup(Map<String,Object> controlGroupConfig) { + double percentage = (controlGroupConfig.get(CONTROL_GROUP_CONFIG_PERCENTAGE) != null && controlGroupConfig.get(CONTROL_GROUP_CONFIG_PERCENTAGE) instanceof Number) ? + ((Number) controlGroupConfig.get(CONTROL_GROUP_CONFIG_PERCENTAGE)).doubleValue() : 0; + + double random = controlGroupRandom.nextDouble() * 100.0; + return random <= percentage; + } +} diff --git a/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java b/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java index 809a724d6..a79bc62bf 100644 --- a/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java +++ b/services/src/main/java/org/apache/unomi/services/sorts/FilterPersonalizationStrategy.java @@ -17,6 +17,7 @@ package org.apache.unomi.services.sorts; +import org.apache.unomi.api.PersonalizationResult; import org.apache.unomi.api.Profile; import org.apache.unomi.api.Session; import org.apache.unomi.api.PersonalizationStrategy; @@ -39,7 +40,7 @@ public class FilterPersonalizationStrategy implements PersonalizationStrategy { } @Override - public List<String> personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest) { + public PersonalizationResult personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest) { List<String> sortedContent = new ArrayList<>(); for (PersonalizationService.PersonalizedContent personalizedContent : personalizationRequest.getContents()) { boolean result = true; @@ -59,6 +60,6 @@ public class FilterPersonalizationStrategy implements PersonalizationStrategy { sortedContent.add(fallback); } - return sortedContent; + return new PersonalizationResult(sortedContent); } } diff --git a/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java b/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java index 3dd31f78b..76bb168af 100644 --- a/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java +++ b/services/src/main/java/org/apache/unomi/services/sorts/RandomPersonalizationStrategy.java @@ -17,19 +17,19 @@ package org.apache.unomi.services.sorts; +import org.apache.unomi.api.PersonalizationResult; import org.apache.unomi.api.Profile; import org.apache.unomi.api.Session; import org.apache.unomi.api.services.PersonalizationService; import java.util.Collections; -import java.util.List; public class RandomPersonalizationStrategy extends FilterPersonalizationStrategy { @Override - public List<String> personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest) { - List<String> r = super.personalizeList(profile, session, personalizationRequest); - Collections.shuffle(r); + public PersonalizationResult personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest) { + PersonalizationResult r = super.personalizeList(profile, session, personalizationRequest); + Collections.shuffle(r.getContentIds()); return r; } } diff --git a/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java b/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java index 82bcc165b..93d6c0ebe 100644 --- a/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java +++ b/services/src/main/java/org/apache/unomi/services/sorts/ScorePersonalizationStrategy.java @@ -17,6 +17,7 @@ package org.apache.unomi.services.sorts; +import org.apache.unomi.api.PersonalizationResult; import org.apache.unomi.api.Profile; import org.apache.unomi.api.Session; import org.apache.unomi.api.PersonalizationStrategy; @@ -35,7 +36,7 @@ public class ScorePersonalizationStrategy implements PersonalizationStrategy { } @Override - public List<String> personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest) { + public PersonalizationResult personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest) { List<String> sortedContent = new ArrayList<>(); final Map<String,Integer> t = new HashMap<>(); @@ -64,7 +65,7 @@ public class ScorePersonalizationStrategy implements PersonalizationStrategy { String scoringPlanList = (String) (personalizedContent.getProperties() != null ? personalizedContent.getProperties().get("scoringPlans") : null); if (scoringPlanList != null) { - Map<String,Integer> scoreValues = (Map<String, Integer>) profile.getScores(); + Map<String,Integer> scoreValues = profile.getScores(); for (String scoringPlan : scoringPlanList.split(" ")) { if (scoreValues.get(scoringPlan) != null) { score += scoreValues.get(scoringPlan); @@ -93,18 +94,14 @@ public class ScorePersonalizationStrategy implements PersonalizationStrategy { sortedContent.add(personalizedContent.getId()); } } - Collections.sort(sortedContent, new Comparator<String>() { - @Override - public int compare(String o1, String o2) { - return t.get(o2) - t.get(o1); - } - }); + + sortedContent.sort((o1, o2) -> t.get(o2) - t.get(o1)); String fallback = (String) personalizationRequest.getStrategyOptions().get("fallback"); if (fallback != null && !sortedContent.contains(fallback)) { sortedContent.add(fallback); } - return sortedContent; + return new PersonalizationResult(sortedContent); } }