You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by sh...@apache.org on 2022/01/17 09:43:49 UTC
[unomi] branch unomi-1.6.x updated: UNOMI-537 Add control groups to personalizations (#368)
This is an automated email from the ASF dual-hosted git repository.
shuber pushed a commit to branch unomi-1.6.x
in repository https://gitbox.apache.org/repos/asf/unomi.git
The following commit(s) were added to refs/heads/unomi-1.6.x by this push:
new 305bfd0 UNOMI-537 Add control groups to personalizations (#368)
305bfd0 is described below
commit 305bfd08db229743ab0d6d411f50c5382cb59359
Author: Serge Huber <sh...@jahia.com>
AuthorDate: Mon Jan 17 10:41:10 2022 +0100
UNOMI-537 Add control groups to personalizations (#368)
- Personalizations will now store a control group election in the session or in the profile
- Updated documentation for personalization
- Implemented integration tests for control groups
(cherry picked from commit 3054bcc2730af37c64cb248e338e69e3ea8acd5c)
---
...ionStrategy.java => PersonalizationResult.java} | 24 +-
.../apache/unomi/api/PersonalizationStrategy.java | 12 +-
.../unomi/api/services/PersonalizationService.java | 3 +-
.../org/apache/unomi/itests/ContextServletIT.java | 83 +-
.../resources/personalization-controlgroup.json | 1031 ++++++++++++++++++++
itests/src/test/resources/personalization.json | 2 +-
.../src/main/asciidoc/samples/twitter-sample.adoc | 29 +-
.../unomi/rest/endpoints/ContextJsonEndpoint.java | 46 +-
.../main/java/org/apache/unomi/utils/Changes.java | 4 +
.../impl/personalization/ControlGroup.java | 68 ++
.../PersonalizationServiceImpl.java | 69 +-
.../sorts/FilterPersonalizationStrategy.java | 3 +
12 files changed, 1326 insertions(+), 48 deletions(-)
diff --git a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java b/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
similarity index 59%
copy from api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
copy to api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
index 9c76d41..446189a 100644
--- a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
+++ b/api/src/main/java/org/apache/unomi/api/PersonalizationResult.java
@@ -16,13 +16,27 @@
*/
package org.apache.unomi.api;
-import org.apache.unomi.api.services.PersonalizationService;
-
import java.util.List;
/**
- *
+ * 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).
*/
-public interface PersonalizationStrategy {
- List<String> personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest);
+public class PersonalizationResult {
+
+ 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;
+ }
}
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 9c76d41..152625e 100644
--- a/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
+++ b/api/src/main/java/org/apache/unomi/api/PersonalizationStrategy.java
@@ -21,8 +21,18 @@ import org.apache.unomi.api.services.PersonalizationService;
import java.util.List;
/**
- *
+ * Interface for personalization strategies. Will filter and reorder the content list according to the strategy
+ * implementation
*/
public interface PersonalizationStrategy {
+
+ /**
+ * Filters and personalizes the list of contents passed as a parameter using the strategy's implementation.
+ * @param profile the profile to use for the personalization
+ * @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
+ */
List<String> personalizeList(Profile profile, Session session, PersonalizationService.PersonalizationRequest personalizationRequest);
}
diff --git a/api/src/main/java/org/apache/unomi/api/services/PersonalizationService.java b/api/src/main/java/org/apache/unomi/api/services/PersonalizationService.java
index ad6e3a6..f4dea3d 100644
--- a/api/src/main/java/org/apache/unomi/api/services/PersonalizationService.java
+++ b/api/src/main/java/org/apache/unomi/api/services/PersonalizationService.java
@@ -17,6 +17,7 @@
package org.apache.unomi.api.services;
+import org.apache.unomi.api.PersonalizationResult;
import org.apache.unomi.api.Profile;
import org.apache.unomi.api.Session;
import org.apache.unomi.api.conditions.Condition;
@@ -57,7 +58,7 @@ public interface PersonalizationService {
* @param personalizationRequest Personalization request, containing the list of variants and the required strategy
* @return List of ids, based on user profile
*/
- List<String> personalizeList(Profile profile, Session session, PersonalizationRequest personalizationRequest);
+ PersonalizationResult personalizeList(Profile profile, Session session, PersonalizationRequest personalizationRequest);
/**
* Personalization request
diff --git a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
index d9e5490..45acb10 100644
--- a/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/ContextServletIT.java
@@ -24,7 +24,6 @@ import org.apache.http.entity.StringEntity;
import org.apache.unomi.api.*;
import org.apache.unomi.api.conditions.Condition;
import org.apache.unomi.api.segments.Scoring;
-import org.apache.unomi.api.segments.ScoringElement;
import org.apache.unomi.api.segments.Segment;
import org.apache.unomi.api.services.DefinitionsService;
import org.apache.unomi.api.services.EventService;
@@ -405,6 +404,88 @@ public class ContextServletIT extends BaseIT {
}
@Test
+ public void testPersonalizationWithControlGroup() throws IOException, InterruptedException {
+
+ Map<String,String> parameters = new HashMap<>();
+ parameters.put("storeInSession", "false");
+ HttpPost request = new HttpPost(URL + CONTEXT_URL);
+ request.setEntity(new StringEntity(getValidatedBundleJSON("personalization-controlgroup.json", parameters), ContentType.create("application/json")));
+ TestUtils.RequestResponse response = TestUtils.executeContextJSONRequest(request);
+ assertEquals("Invalid response code", 200, response.getStatusCode());
+ refreshPersistence();
+ Thread.sleep(2000); //Making sure event is updated in DB
+ ContextResponse contextResponse = response.getContextResponse();
+
+ Map<String,List<String>> personalizations = contextResponse.getPersonalizations();
+
+ validatePersonalizations(personalizations);
+
+ // let's check that the persisted profile has the control groups;
+ Map<String,Object> profileProperties = contextResponse.getProfileProperties();
+ List<Map<String,Object>> profileControlGroups = (List<Map<String,Object>>) profileProperties.get("unomiControlGroups");
+ assertControlGroups(profileControlGroups);
+
+ Profile updatedProfile = profileService.load(contextResponse.getProfileId());
+ profileControlGroups = (List<Map<String,Object>>) updatedProfile.getProperty("unomiControlGroups");
+ assertNotNull("Profile control groups not found in persisted profile", profileControlGroups);
+ assertControlGroups(profileControlGroups);
+
+ // now let's test with session storage
+ parameters.put("storeInSession", "true");
+ request = new HttpPost(URL + CONTEXT_URL);
+ request.setEntity(new StringEntity(getValidatedBundleJSON("personalization-controlgroup.json", parameters), ContentType.create("application/json")));
+ response = TestUtils.executeContextJSONRequest(request);
+ assertEquals("Invalid response code", 200, response.getStatusCode());
+ refreshPersistence();
+ Thread.sleep(2000); //Making sure event is updated in DB
+ contextResponse = response.getContextResponse();
+
+ personalizations = contextResponse.getPersonalizations();
+
+ validatePersonalizations(personalizations);
+
+ Map<String,Object> sessionProperties = contextResponse.getSessionProperties();
+ List<Map<String,Object>> sessionControlGroups = (List<Map<String,Object>>) sessionProperties.get("unomiControlGroups");
+ assertControlGroups(sessionControlGroups);
+
+ Session updatedSession = profileService.loadSession(contextResponse.getSessionId(), new Date());
+ sessionControlGroups = (List<Map<String,Object>>) updatedSession.getProperty("unomiControlGroups");
+ assertNotNull("Session control groups not found in persisted session", sessionControlGroups);
+ assertControlGroups(sessionControlGroups);
+
+ }
+
+ private void validatePersonalizations(Map<String, List<String>> personalizations) {
+ assertEquals("Personalizations don't have expected size", 2, personalizations.size());
+
+ List<String> perso1Contents = personalizations.get("perso1");
+ assertEquals("Perso 1 content list size doesn't match", 10, perso1Contents.size());
+ List<String> expectedPerso1Contents = new ArrayList<>();
+ expectedPerso1Contents.add("perso1content1");
+ expectedPerso1Contents.add("perso1content2");
+ expectedPerso1Contents.add("perso1content3");
+ expectedPerso1Contents.add("perso1content4");
+ expectedPerso1Contents.add("perso1content5");
+ expectedPerso1Contents.add("perso1content6");
+ expectedPerso1Contents.add("perso1content7");
+ expectedPerso1Contents.add("perso1content8");
+ expectedPerso1Contents.add("perso1content9");
+ expectedPerso1Contents.add("perso1content10");
+ assertEquals("Perso1 contents do not match", expectedPerso1Contents, perso1Contents);
+ }
+
+ private void assertControlGroups(List<Map<String, Object>> profileControlGroups) {
+ assertNotNull("Couldn't find control groups for profile", profileControlGroups);
+ assertTrue("Control group size should be 1", profileControlGroups.size() == 1);
+ Map<String,Object> controlGroup = profileControlGroups.get(0);
+ assertEquals("Invalid ID for control group", "perso1", controlGroup.get("id"));
+ assertEquals("Invalid path for control group", "/home/perso1.html", controlGroup.get("path"));
+ assertEquals("Invalid displayName for control group", "First perso", controlGroup.get("displayName"));
+ assertNotNull("Null timestamp for control group", controlGroup.get("timeStamp"));
+ }
+
+
+ @Test
public void testRequireScoring() throws IOException, InterruptedException {
Map<String,String> parameters = new HashMap<>();
diff --git a/itests/src/test/resources/personalization-controlgroup.json b/itests/src/test/resources/personalization-controlgroup.json
new file mode 100644
index 0000000..8a931d7
--- /dev/null
+++ b/itests/src/test/resources/personalization-controlgroup.json
@@ -0,0 +1,1031 @@
+{
+ "source": {
+ "itemId": "CMSServer",
+ "itemType": "custom",
+ "scope": "acme",
+ "version": null,
+ "properties": {}
+ },
+ "requireSegments": true,
+ "requiredProfileProperties": [
+ "unomiControlGroups"
+ ],
+ "requiredSessionProperties": [
+ "unomiControlGroups"
+ ],
+ "events": null,
+ "filters": null,
+ "personalizations": [
+ {
+ "id": "perso1",
+ "strategy": "score-sorted",
+ "strategyOptions": {
+ "threshold": -1,
+ "controlGroup" : {
+ "percentage" : 1.0,
+ "displayName" : "First perso",
+ "path" : "/home/perso1.html",
+ "storeInSession" : ###storeInSession###
+ }
+ },
+ "contents": [
+ {
+ "id": "perso1content1",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": {
+ "interests": "health food"
+ }
+ },
+ {
+ "id": "perso1content2",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/contactus.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso1content3",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/documentation.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso1content4",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/aboutus.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso1content5",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/products.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso1content6",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/services.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso1content7",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/community.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso1content8",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/projects.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso1content9",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/home.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso1content10",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/theend.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ }
+ ]
+ },
+ {
+ "id": "perso2",
+ "strategy": "score-sorted",
+ "strategyOptions": {
+ "threshold": -1
+ },
+ "contents": [
+ {
+ "id": "perso2content1",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": {
+ "interests": "health food"
+ }
+ },
+ {
+ "id": "perso2content2",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/contactus.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso1content3",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/documentation.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso2content4",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/aboutus.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso2content5",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/products.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso2content6",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/services.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso2content7",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/community.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso2content8",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/projects.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso2content9",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/home.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ },
+ {
+ "id": "perso2content10",
+ "filters": [
+ {
+ "appliesOn": null,
+ "condition": {
+ "parameterValues": {
+ "minimumEventCount": 1,
+ "eventCondition": {
+ "type": "booleanCondition",
+ "parameterValues": {
+ "operator": "and",
+ "subConditions" : [
+ {
+ "type": "eventTypeCondition",
+ "parameterValues": {
+ "eventTypeId": "view"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.pagePath",
+ "propertyValue": "/theend.html",
+ "comparisonOperator": "equals"
+ }
+ },
+ {
+ "type": "eventPropertyCondition",
+ "parameterValues": {
+ "propertyName": "target.properties.pageInfo.language",
+ "propertyValue": "en",
+ "comparisonOperator": "equals"
+ }
+ }
+ ]
+ }
+ },
+ "numberOfDays": 30
+ },
+ "type": "pastEventCondition"
+ },
+ "properties": {
+ "score": -1000
+ }
+ }
+ ],
+ "properties": null
+ }
+ ]
+ }
+ ],
+ "profileOverrides": null,
+ "sessionPropertiesOverrides": null,
+ "sessionId": "test-session-id"
+}
\ No newline at end of file
diff --git a/itests/src/test/resources/personalization.json b/itests/src/test/resources/personalization.json
index d0461e9..89ac156 100644
--- a/itests/src/test/resources/personalization.json
+++ b/itests/src/test/resources/personalization.json
@@ -8,7 +8,7 @@
},
"requireSegments": true,
"requiredProfileProperties": [
- "interests"
+ "*"
],
"requiredSessionProperties": [
"*"
diff --git a/manual/src/main/asciidoc/samples/twitter-sample.adoc b/manual/src/main/asciidoc/samples/twitter-sample.adoc
index ac1057d..7d7eeba 100644
--- a/manual/src/main/asciidoc/samples/twitter-sample.adoc
+++ b/manual/src/main/asciidoc/samples/twitter-sample.adoc
@@ -288,7 +288,7 @@ curl --location --request POST 'http://localhost:8181/context.json' \
"source": null,
"requireSegments": false,
"requiredProfileProperties": null,
- "requiredSessionProperties": null,
+ "requiredSessionProperties": [ "unomiControlGroups" ],
"events": null,
"filters": null,
"personalizations": [
@@ -296,7 +296,13 @@ curl --location --request POST 'http://localhost:8181/context.json' \
"id": "gender-test",
"strategy": "matching-first",
"strategyOptions": {
- "fallback": "var2"
+ "fallback": "var2",
+ "controlGroup" : {
+ "percentage" : 0.1,
+ "displayName" : "Gender test control group",
+ "path" : "/gender-test",
+ "storeInSession" : true
+ }
},
"contents": [
{
@@ -333,13 +339,12 @@ curl --location --request POST 'http://localhost:8181/context.json' \
In the above example, we basically setup two variants : `var1` and `var2` and setup the `var2` to be the fallback variant
in case no variant is matched. We could of course specify more than a variant. The `strategy` indicates to the
-personalization service how to calculate the "winning" variant. In this case the strategy `matching-first` will return
-the first variant that matches the current profile.
+personalization service how to calculate the "winning" variant. In this case the strategy `matching-first` will return variants that match the current profile. We also use the `controlGroups` option to specify that we want to have a control group for this personalization. The `0.1` percentage value represents 10% (0 to 1) of traffic that will be assigned randomly to the control group. The control group will be stored in the profile and the session of the visitors if they were assigned to [...]
Currently the following strategies are available:
-- `matching-first`: will return the first matching variant.
-- `random`: will return a random variant
+- `matching-first`: will return the variant IDs that match the current profile (using the initial content order)
+- `random`: will return a shuffled list of variant IDs (ignoring any conditions)
- `score-sorted`: allows to sort the variants based on scores associated with the filtering conditions, effectively
sorting them by the highest scoring condition first.
@@ -351,7 +356,16 @@ Here is the result of the above example:
"profileId": "01060c4c-a055-4c8f-9692-8a699d0c434a",
"sessionId": "demo-session-id",
"profileProperties": null,
- "sessionProperties": null,
+ "sessionProperties": {
+ "unomiControlGroups": [
+ {
+ "id": "previousPerso",
+ "displayName": "Previous perso",
+ "path": "/home/previousPerso.html",
+ "timeStamp": "2021-12-15T13:52:38Z"
+ }
+ ]
+ },
"profileSegments": null,
"filteringResults": null,
"processedEvents": 0,
@@ -367,6 +381,7 @@ Here is the result of the above example:
}
----
+In the above example we can see the profile and session were assigned to other control groups but not the current one (the ids are different).
====== Overrides
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 525dd3a..6648d9a 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
@@ -20,13 +20,7 @@ package org.apache.unomi.rest.endpoints;
import com.fasterxml.jackson.core.JsonProcessingException;
import org.apache.commons.lang3.StringUtils;
import org.apache.cxf.rs.security.cors.CrossOriginResourceSharing;
-import org.apache.unomi.api.ContextRequest;
-import org.apache.unomi.api.ContextResponse;
-import org.apache.unomi.api.Event;
-import org.apache.unomi.api.Persona;
-import org.apache.unomi.api.PersonaWithSessions;
-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.ConfigSharingService;
import org.apache.unomi.api.services.EventService;
@@ -367,6 +361,26 @@ public class ContextJsonEndpoint {
Changes changes = restServiceUtils.handleEvents(contextRequest.getEvents(), session, profile, request, response, timestamp);
data.setProcessedEvents(changes.getProcessedItems());
+ List<PersonalizationService.PersonalizedContent> filterNodes = contextRequest.getFilters();
+ if (filterNodes != null) {
+ data.setFilteringResults(new HashMap<>());
+ for (PersonalizationService.PersonalizedContent personalizedContent : sanitizePersonalizedContentObjects(filterNodes)) {
+ data.getFilteringResults()
+ .put(personalizedContent.getId(), personalizationService.filter(profile, session, personalizedContent));
+ }
+ }
+
+ List<PersonalizationService.PersonalizationRequest> personalizations = contextRequest.getPersonalizations();
+ if (personalizations != null) {
+ data.setPersonalizations(new HashMap<>());
+ for (PersonalizationService.PersonalizationRequest personalization : sanitizePersonalizations(personalizations)) {
+ PersonalizationResult personalizationResult = personalizationService.personalizeList(profile, session, personalization);
+ changes.setChangeType(changes.getChangeType() | personalizationResult.getChangeType());
+ data.getPersonalizations()
+ .put(personalization.getId(), personalizationResult.getContentIds());
+ }
+ }
+
profile = changes.getProfile();
if (contextRequest.isRequireSegments()) {
@@ -397,24 +411,6 @@ public class ContextJsonEndpoint {
processOverrides(contextRequest, profile, session);
- List<PersonalizationService.PersonalizedContent> filterNodes = contextRequest.getFilters();
- if (filterNodes != null) {
- data.setFilteringResults(new HashMap<>());
- for (PersonalizationService.PersonalizedContent personalizedContent : sanitizePersonalizedContentObjects(filterNodes)) {
- data.getFilteringResults()
- .put(personalizedContent.getId(), personalizationService.filter(profile, session, personalizedContent));
- }
- }
-
- List<PersonalizationService.PersonalizationRequest> personalizations = contextRequest.getPersonalizations();
- if (personalizations != null) {
- data.setPersonalizations(new HashMap<>());
- for (PersonalizationService.PersonalizationRequest personalization : sanitizePersonalizations(personalizations)) {
- data.getPersonalizations()
- .put(personalization.getId(), personalizationService.personalizeList(profile, session, personalization));
- }
- }
-
if (!(profile instanceof Persona)) {
data.setTrackedConditions(rulesService.getTrackedConditions(contextRequest.getSource()));
} else {
diff --git a/rest/src/main/java/org/apache/unomi/utils/Changes.java b/rest/src/main/java/org/apache/unomi/utils/Changes.java
index 3ed75a6..b333430 100644
--- a/rest/src/main/java/org/apache/unomi/utils/Changes.java
+++ b/rest/src/main/java/org/apache/unomi/utils/Changes.java
@@ -43,6 +43,10 @@ public class Changes {
return changeType;
}
+ public void setChangeType(int changeType) {
+ this.changeType = changeType;
+ }
+
public int getProcessedItems() {
return processedItems;
}
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
new file mode 100644
index 0000000..d6435d0
--- /dev/null
+++ b/services/src/main/java/org/apache/unomi/services/impl/personalization/ControlGroup.java
@@ -0,0 +1,68 @@
+/*
+ * 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 java.util.Date;
+
+/**
+ * Represents a personalization control group, stored in a profile and/or a session
+ */
+public class ControlGroup {
+ 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 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;
+ }
+}
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 a4ae8f3..eeef772 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,18 +17,20 @@
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.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.osgi.framework.BundleContext;
import org.osgi.framework.ServiceReference;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
public class PersonalizationServiceImpl implements PersonalizationService {
@@ -37,6 +39,8 @@ public class PersonalizationServiceImpl implements PersonalizationService {
private Map<String, PersonalizationStrategy> personalizationStrategies = new ConcurrentHashMap<>();
+ private Random controlGroupRandom = new Random();
+
public void setProfileService(ProfileService profileService) {
this.profileService = profileService;
}
@@ -73,19 +77,70 @@ public class PersonalizationServiceImpl implements PersonalizationService {
@Override
public String bestMatch(Profile profile, Session session, PersonalizationRequest personalizationRequest) {
- List<String> sorted = personalizeList(profile,session,personalizationRequest);
- if (sorted.size() > 0) {
- return sorted.get(0);
+ PersonalizationResult result = personalizeList(profile,session,personalizationRequest);
+ if (result.getContentIds().size() > 0) {
+ return result.getContentIds().get(0);
}
return null;
}
@Override
- public List<String> personalizeList(Profile profile, Session session, PersonalizationRequest personalizationRequest) {
+ public PersonalizationResult personalizeList(Profile profile, Session session, PersonalizationRequest personalizationRequest) {
PersonalizationStrategy strategy = personalizationStrategies.get(personalizationRequest.getStrategy());
+ int changeType = EventService.NO_CHANGE;
if (strategy != null) {
- return strategy.personalizeList(profile, session, personalizationRequest);
+ 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");
+ }
+
+ boolean profileInControlGroup = false;
+ Optional<ControlGroup> currentControlGroup;
+
+ List<ControlGroup> controlGroups = null;
+ if (storeInSession) {
+ controlGroups = (List<ControlGroup>) session.getProperty("unomiControlGroups");
+ } else {
+ controlGroups = (List<ControlGroup>) profile.getProperty("unomiControlGroups");
+ }
+ 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();
+ Double controlGroupPercentage = (Double) controlGroupMap.get("percentage");
+
+ 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);
+ if (storeInSession) {
+ session.setProperty("unomiControlGroups", controlGroups);
+ changeType = EventService.SESSION_UPDATED;
+ } else {
+ profile.setProperty("unomiControlGroups", controlGroups);
+ 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);
+ }
+ }
+ return new PersonalizationResult(strategy.personalizeList(profile, session, personalizationRequest), changeType);
}
throw new IllegalArgumentException("Unknown strategy : "+ personalizationRequest.getStrategy());
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 41ac9b5..809a724 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
@@ -27,6 +27,9 @@ import org.apache.unomi.api.services.ProfileService;
import java.util.ArrayList;
import java.util.List;
+/**
+ * This strategy will use filters to only keep the contents that match all their associated filters
+ */
public class FilterPersonalizationStrategy implements PersonalizationStrategy {
private ProfileService profileService;