You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@unomi.apache.org by jk...@apache.org on 2022/01/28 15:37:52 UTC

[unomi] 01/01: UNOMI-550: handle negative past event condition and code cleaning

This is an automated email from the ASF dual-hosted git repository.

jkevan pushed a commit to branch handleNegativePastEventCondition
in repository https://gitbox.apache.org/repos/asf/unomi.git

commit a734dc7cad55fd2dc4cfe5a53b7aad9181960b4a
Author: Kevan <ke...@jahia.com>
AuthorDate: Fri Jan 28 16:37:37 2022 +0100

    UNOMI-550: handle negative past event condition and code cleaning
---
 .../java/org/apache/unomi/itests/SegmentIT.java    |  45 +++++
 .../conditions/IdsConditionESQueryBuilder.java     |  57 ++++++
 .../conditions/IdsConditionEvaluator.java          |  36 ++++
 .../PastEventConditionESQueryBuilder.java          | 218 ++++++++++++---------
 .../conditions/PastEventConditionEvaluator.java    |  72 +------
 .../META-INF/cxs/conditions/IdsCondition.json      |  23 +++
 .../cxs/conditions/pastEventCondition.json         |   5 +
 .../resources/OSGI-INF/blueprint/blueprint.xml     |  17 ++
 8 files changed, 314 insertions(+), 159 deletions(-)

diff --git a/itests/src/test/java/org/apache/unomi/itests/SegmentIT.java b/itests/src/test/java/org/apache/unomi/itests/SegmentIT.java
index 55f3ce9..781cce1 100644
--- a/itests/src/test/java/org/apache/unomi/itests/SegmentIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/SegmentIT.java
@@ -192,6 +192,51 @@ public class SegmentIT extends BaseIT {
     }
 
     @Test
+    public void testSegmentWithNegativePastEventCondition() throws InterruptedException {
+        // create Profile
+        Profile profile = new Profile();
+        profile.setItemId("test_profile_id");
+        profileService.save(profile);
+        persistenceService.refreshIndex(Profile.class, null); // wait for profile to be full persisted and index
+
+        // create the negative past event condition segment
+        Metadata segmentMetadata = new Metadata("negative-past-event-segment-test");
+        Segment segment = new Segment(segmentMetadata);
+        Condition segmentCondition = new Condition(definitionsService.getConditionType("pastEventCondition"));
+        segmentCondition.setParameter("numberOfDays", 10);
+        Condition pastEventEventCondition = new Condition(definitionsService.getConditionType("eventTypeCondition"));
+        pastEventEventCondition.setParameter("eventTypeId", "negative-test-event-type");
+        segmentCondition.setParameter("eventCondition", pastEventEventCondition);
+        segmentCondition.setParameter("operator", "eventsNotOccurred");
+        segment.setCondition(segmentCondition);
+        segmentService.setSegmentDefinition(segment);
+
+        // insure that profile is correctly engaged in sement since there is no events yet.
+        keepTrying("Profile should be engaged in the segment, there is no event for the past condition yet",
+                () -> profileService.load("test_profile_id"),
+                updatedProfile -> updatedProfile.getSegments().contains("negative-past-event-segment-test"),
+                1000, 20);
+
+        // send event for profile from a previous date (today -3 days)
+        ZoneId defaultZoneId = ZoneId.systemDefault();
+        LocalDate localDate = LocalDate.now().minusDays(3);
+        Event testEvent = new Event("negative-test-event-type", null, profile, null, null, profile, Date.from(localDate.atStartOfDay(defaultZoneId).toInstant()));
+        testEvent.setPersistent(true);
+        int changes = eventService.send(testEvent);
+        if ((changes & EventService.PROFILE_UPDATED) == EventService.PROFILE_UPDATED) {
+            profileService.save(profile);
+            persistenceService.refreshIndex(Profile.class, null);
+        }
+        persistenceService.refreshIndex(Event.class, testEvent.getTimeStamp()); // wait for event to be fully persisted and indexed
+
+        // now Profile should be out of the segment since one event have been done and the past event is only valid for no events occurrences
+        keepTrying("Profile should not be engaged in the segment anymore, it have a least one event now",
+                () -> profileService.load("test_profile_id"),
+                updatedProfile -> !updatedProfile.getSegments().contains("negative-past-event-segment-test"),
+                1000, 20);
+    }
+
+    @Test
     public void testSegmentPastEventRecalculation() throws Exception {
         // create Profile
         Profile profile = new Profile();
diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/IdsConditionESQueryBuilder.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/IdsConditionESQueryBuilder.java
new file mode 100644
index 0000000..d535c1b
--- /dev/null
+++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/IdsConditionESQueryBuilder.java
@@ -0,0 +1,57 @@
+/*
+ * 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.plugins.baseplugin.conditions;
+
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBuilder;
+import org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBuilderDispatcher;
+import org.elasticsearch.index.query.BoolQueryBuilder;
+import org.elasticsearch.index.query.IdsQueryBuilder;
+import org.elasticsearch.index.query.QueryBuilder;
+import org.elasticsearch.index.query.QueryBuilders;
+
+import java.util.Collection;
+import java.util.Map;
+
+public class IdsConditionESQueryBuilder implements ConditionESQueryBuilder {
+
+    private int maximumIdsQueryCount = 5000;
+
+    public void setMaximumIdsQueryCount(int maximumIdsQueryCount) {
+        this.maximumIdsQueryCount = maximumIdsQueryCount;
+    }
+
+    @Override
+    public QueryBuilder buildQuery(Condition condition, Map<String, Object> context, ConditionESQueryBuilderDispatcher dispatcher) {
+        Collection<String> ids = (Collection<String>) condition.getParameter("ids");
+        Boolean match = (Boolean) condition.getParameter("match");
+
+        if (ids.size() > maximumIdsQueryCount) {
+            // Avoid building too big ids query - throw exception instead
+            throw new UnsupportedOperationException("Too many profiles");
+        }
+
+        IdsQueryBuilder idsQueryBuilder = QueryBuilders.idsQuery().addIds(ids.toArray(new String[0]));
+        if (match) {
+            return idsQueryBuilder;
+        } else {
+            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
+            boolQuery.mustNot(idsQueryBuilder);
+            return boolQuery;
+        }
+    }
+}
diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/IdsConditionEvaluator.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/IdsConditionEvaluator.java
new file mode 100644
index 0000000..d4f1ca8
--- /dev/null
+++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/IdsConditionEvaluator.java
@@ -0,0 +1,36 @@
+/*
+ * 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.plugins.baseplugin.conditions;
+
+import org.apache.unomi.api.Item;
+import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator;
+import org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluatorDispatcher;
+
+import java.util.Collection;
+import java.util.Map;
+
+public class IdsConditionEvaluator implements ConditionEvaluator {
+    @Override
+    public boolean eval(Condition condition, Item item, Map<String, Object> context, ConditionEvaluatorDispatcher dispatcher) {
+        Collection<String> ids = (Collection<String>) condition.getParameter("ids");
+        Boolean match = (Boolean) condition.getParameter("match");
+
+        boolean contained = ids != null && !ids.isEmpty() && ids.contains(item.getItemId());
+        return match == contained;
+    }
+}
diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionESQueryBuilder.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionESQueryBuilder.java
index c76a750..4491eb6 100644
--- a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionESQueryBuilder.java
+++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionESQueryBuilder.java
@@ -20,6 +20,7 @@ package org.apache.unomi.plugins.baseplugin.conditions;
 import org.apache.unomi.api.Event;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.conditions.Condition;
+import org.apache.unomi.api.conditions.ConditionType;
 import org.apache.unomi.api.services.DefinitionsService;
 import org.apache.unomi.api.services.SegmentService;
 import org.apache.unomi.persistence.elasticsearch.conditions.ConditionContextHelper;
@@ -28,9 +29,7 @@ import org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBui
 import org.apache.unomi.persistence.spi.PersistenceService;
 import org.apache.unomi.persistence.spi.aggregate.TermsAggregate;
 import org.apache.unomi.scripting.ScriptExecutor;
-import org.elasticsearch.index.query.QueryBuilder;
-import org.elasticsearch.index.query.QueryBuilders;
-import org.elasticsearch.index.query.RangeQueryBuilder;
+import org.elasticsearch.index.query.*;
 
 import java.util.*;
 import java.util.stream.Collectors;
@@ -74,113 +73,137 @@ public class PastEventConditionESQueryBuilder implements ConditionESQueryBuilder
         this.segmentService = segmentService;
     }
 
+    @Override
     public QueryBuilder buildQuery(Condition condition, Map<String, Object> context, ConditionESQueryBuilderDispatcher dispatcher) {
-        Integer minimumEventCount = condition.getParameter("minimumEventCount") == null ? 1 : (Integer) condition.getParameter("minimumEventCount");
-        Integer maximumEventCount = condition.getParameter("maximumEventCount") == null ? Integer.MAX_VALUE : (Integer) condition.getParameter("maximumEventCount");
+        boolean eventsOccurred = getStrategyFromOperator((String) condition.getParameter("operator"));
+        int minimumEventCount = !eventsOccurred || condition.getParameter("minimumEventCount") == null ? 1 : (Integer) condition.getParameter("minimumEventCount");
+        int maximumEventCount = !eventsOccurred || condition.getParameter("maximumEventCount") == null ? Integer.MAX_VALUE : (Integer) condition.getParameter("maximumEventCount");
         String generatedPropertyKey = (String) condition.getParameter("generatedPropertyKey");
 
         if (generatedPropertyKey != null && generatedPropertyKey.equals(segmentService.getGeneratedPropertyKey((Condition) condition.getParameter("eventCondition"), condition))) {
             // A property is already set on profiles matching the past event condition, use it to check the numbers of occurrences
-            RangeQueryBuilder builder = QueryBuilders.rangeQuery("systemProperties.pastEvents." + generatedPropertyKey);
-            builder.gte(minimumEventCount);
-            builder.lte(maximumEventCount);
-            return builder;
+            return dispatcher.buildFilter(getProfileConditionForCounter(generatedPropertyKey, minimumEventCount, maximumEventCount, eventsOccurred), context);
         } else {
             // No property set - tries to build an idsQuery
-            // Build past event condition
-            Condition eventCondition = getEventCondition(condition, context);
-
-            Set<String> ids = new HashSet<>();
-
-            if (pastEventsDisablePartitions) {
-                Map<String, Long> eventCountByProfile = persistenceService.aggregateWithOptimizedQuery(eventCondition, new TermsAggregate("profileId"), Event.ITEM_TYPE, maximumIdsQueryCount);
-                ids = eventCountByProfile.entrySet().stream()
-                        .filter(x -> !x.getKey().equals("_filtered"))
-                        .filter(x -> x.getValue() >= minimumEventCount && x.getValue() <= maximumEventCount)
-                        .map(Map.Entry::getKey)
-                        .collect(Collectors.toSet());
-            } else {
-                // Get full cardinality to partition the terms aggreggation
-                Map<String, Double> m = persistenceService.getSingleValuesMetrics(eventCondition, new String[]{"card"}, "profileId.keyword", Event.ITEM_TYPE);
-                long card = m.get("_card").longValue();
-
-                int numParts = (int) (card / aggregateQueryBucketSize) + 2;
-                for (int i = 0; i < numParts; i++) {
-                    Map<String, Long> eventCountByProfile = persistenceService.aggregateWithOptimizedQuery(eventCondition, new TermsAggregate("profileId", i, numParts), Event.ITEM_TYPE);
-                    if (eventCountByProfile != null) {
-                        eventCountByProfile.remove("_filtered");
-                        for (Map.Entry<String, Long> entry : eventCountByProfile.entrySet()) {
-                            if (entry.getValue() < minimumEventCount) {
-                                // No more interesting buckets in this partition
-                                break;
-                            } else if (entry.getValue() <= maximumEventCount) {
-                                ids.add(entry.getKey());
+            // TODO see for deprecation, this should not happen anymore each past event condition should have a generatedPropertyKey
+            Condition eventCondition = getEventCondition(condition, context, null, definitionsService, scriptExecutor);
+            Set<String> ids = getProfileIdsMatchingEventCount(eventCondition, minimumEventCount, maximumEventCount);
+            return dispatcher.buildFilter(getProfileIdsCondition(ids, eventsOccurred), context);
+        }
+    }
 
-                                if (ids.size() > maximumIdsQueryCount) {
-                                    // Avoid building too big ids query - throw exception instead
-                                    throw new UnsupportedOperationException("Too many profiles");
-                                }
-                            }
-                        }
-                    }
-                }
+    @Override
+    public long count(Condition condition, Map<String, Object> context, ConditionESQueryBuilderDispatcher dispatcher) {
+        boolean eventsOccurred = getStrategyFromOperator((String) condition.getParameter("operator"));
+        int minimumEventCount = !eventsOccurred || condition.getParameter("minimumEventCount") == null ? 1 : (Integer) condition.getParameter("minimumEventCount");
+        int maximumEventCount = !eventsOccurred || condition.getParameter("maximumEventCount") == null ? Integer.MAX_VALUE : (Integer) condition.getParameter("maximumEventCount");
+        String generatedPropertyKey = (String) condition.getParameter("generatedPropertyKey");
+
+        if (generatedPropertyKey != null && generatedPropertyKey.equals(segmentService.getGeneratedPropertyKey((Condition) condition.getParameter("eventCondition"), condition))) {
+            // query profiles directly
+            return persistenceService.queryCount(getProfileConditionForCounter(generatedPropertyKey, minimumEventCount, maximumEventCount, eventsOccurred), Profile.ITEM_TYPE);
+        } else {
+            // No count filter - simply get the full number of distinct profiles
+            // TODO see for deprecation, this should not happen anymore each past event condition should have a generatedPropertyKey
+            Condition eventCondition = getEventCondition(condition, context, null, definitionsService, scriptExecutor);
+            if (eventsOccurred && minimumEventCount == 1 && maximumEventCount == Integer.MAX_VALUE) {
+                return persistenceService.getSingleValuesMetrics(eventCondition, new String[]{"card"}, "profileId.keyword", Event.ITEM_TYPE).get("_card").longValue();
             }
 
-            return QueryBuilders.idsQuery().addIds(ids.toArray(new String[0]));
+            Set<String> profileIds = getProfileIdsMatchingEventCount(eventCondition, minimumEventCount, maximumEventCount);
+            return eventsOccurred ? profileIds.size() : persistenceService.queryCount(getProfileIdsCondition(profileIds, false), Profile.ITEM_TYPE);
         }
     }
 
-    public long count(Condition condition, Map<String, Object> context, ConditionESQueryBuilderDispatcher dispatcher) {
-        Condition eventCondition = getEventCondition(condition, context);
-        Map<String, Double> aggResult = null;
+    protected static boolean getStrategyFromOperator(String operator) {
+        if (operator != null && !operator.equals("eventsOccurred") && !operator.equals("eventsNotOccurred")) {
+            throw new UnsupportedOperationException("Unsupported operator: " + operator + ", please use either 'eventsOccurred' or 'eventsNotOccurred'");
+        }
+        return operator == null || operator.equals("eventsOccurred");
+    }
+
+    private Condition getProfileIdsCondition(Set<String> ids, boolean shouldMatch) {
+        Condition idsCondition = new Condition();
+        idsCondition.setConditionType(definitionsService.getConditionType("idsCondition"));
+        idsCondition.setParameter("ids", ids);
+        idsCondition.setParameter("match", shouldMatch);
+        return idsCondition;
+    }
+
+    private Condition getProfileConditionForCounter(String generatedPropertyKey, Integer minimumEventCount, Integer maximumEventCount, boolean eventsOccurred) {
+        String generatedPropertyName = "systemProperties.pastEvents." + generatedPropertyKey;
+        ConditionType profilePropertyConditionType = definitionsService.getConditionType("profilePropertyCondition");
+        if (eventsOccurred) {
+            Condition counterIsBetweenBoundaries = new Condition();
+            counterIsBetweenBoundaries.setConditionType(profilePropertyConditionType);
+            counterIsBetweenBoundaries.setParameter("propertyName", generatedPropertyName);
+            counterIsBetweenBoundaries.setParameter("comparisonOperator", "between");
+            counterIsBetweenBoundaries.setParameter("propertyValuesInteger", Arrays.asList(minimumEventCount, maximumEventCount));
+            return counterIsBetweenBoundaries;
+        } else {
+            Condition counterMissing = new Condition();
+            counterMissing.setConditionType(profilePropertyConditionType);
+            counterMissing.setParameter("propertyName", generatedPropertyName);
+            counterMissing.setParameter("comparisonOperator", "missing");
 
-        Integer minimumEventCount = condition.getParameter("minimumEventCount") == null ? 1 : (Integer) condition.getParameter("minimumEventCount");
-        Integer maximumEventCount = condition.getParameter("maximumEventCount") == null ? Integer.MAX_VALUE : (Integer) condition.getParameter("maximumEventCount");
+            Condition counterZero = new Condition();
+            counterZero.setConditionType(profilePropertyConditionType);
+            counterZero.setParameter("propertyName", generatedPropertyName);
+            counterZero.setParameter("comparisonOperator", "equals");
+            counterZero.setParameter("propertyValueInteger", 0);
 
-        // No count filter - simply get the full number of distinct profiles
-        if (minimumEventCount == 1 && maximumEventCount == Integer.MAX_VALUE) {
-            aggResult = persistenceService.getSingleValuesMetrics(eventCondition, new String[]{"card"}, "profileId.keyword", Event.ITEM_TYPE);
-            return aggResult.get("_card").longValue();
+            Condition counterCondition = new Condition();
+            counterCondition.setConditionType(definitionsService.getConditionType("booleanCondition"));
+            counterCondition.setParameter("operator", "or");
+            counterCondition.setParameter("subConditions", Arrays.asList(counterMissing, counterZero));
+            return counterCondition;
         }
+    }
 
+    private Set<String> getProfileIdsMatchingEventCount(Condition eventCondition, int minimumEventCount, int maximumEventCount) {
+        boolean noBoundaries = minimumEventCount == 1 && maximumEventCount == Integer.MAX_VALUE;
         if (pastEventsDisablePartitions) {
             Map<String, Long> eventCountByProfile = persistenceService.aggregateWithOptimizedQuery(eventCondition, new TermsAggregate("profileId"), Event.ITEM_TYPE, maximumIdsQueryCount);
-            return eventCountByProfile.entrySet().stream()
-                    .filter(x -> x.getKey().equals("_filtered"))
-                    .filter(x -> x.getValue() >= minimumEventCount && x.getValue() <= maximumEventCount)
-                    .count();
+            eventCountByProfile.remove("_filtered");
+            return noBoundaries ?
+                    eventCountByProfile.keySet() :
+                    eventCountByProfile.entrySet()
+                            .stream()
+                            .filter(eventCountPerProfile -> (eventCountPerProfile.getValue() >= minimumEventCount && eventCountPerProfile.getValue() <= maximumEventCount))
+                            .map(Map.Entry::getKey)
+                            .collect(Collectors.toSet());
         } else {
-            // Get full cardinality to partition the terms aggreggation
-            aggResult = persistenceService.getSingleValuesMetrics(eventCondition, new String[]{"card"}, "profileId.keyword", Event.ITEM_TYPE);
-            long card = aggResult.get("_card").longValue();
-             // Event count specified, must check occurences count for each profile
-            int result = 0;
+            Set<String> result = new HashSet<>();
+            // Get full cardinality to partition the terms aggregation
+            Map<String, Double> m = persistenceService.getSingleValuesMetrics(eventCondition, new String[]{"card"}, "profileId.keyword", Event.ITEM_TYPE);
+            long card = m.get("_card").longValue();
+
             int numParts = (int) (card / aggregateQueryBucketSize) + 2;
             for (int i = 0; i < numParts; i++) {
                 Map<String, Long> eventCountByProfile = persistenceService.aggregateWithOptimizedQuery(eventCondition, new TermsAggregate("profileId", i, numParts), Event.ITEM_TYPE);
-                int j = 0;
                 if (eventCountByProfile != null) {
                     eventCountByProfile.remove("_filtered");
-                    for (Map.Entry<String, Long> entry : eventCountByProfile.entrySet()) {
-                        if (entry.getValue() < minimumEventCount) {
-                            // No more interesting buckets in this partition
-                            break;
-                        } else if (entry.getValue() <= maximumEventCount && minimumEventCount == 1) {
-                            // Take all remaining elements
-                            result += eventCountByProfile.size() - j;
-                            break;
-                        } else if (entry.getValue() <= maximumEventCount) {
-                            result++;
+                    if (noBoundaries) {
+                        result.addAll(eventCountByProfile.keySet());
+                    } else {
+                        for (Map.Entry<String, Long> entry : eventCountByProfile.entrySet()) {
+                            if (entry.getValue() < minimumEventCount) {
+                                // No more interesting buckets in this partition
+                                break;
+                            } else if (entry.getValue() <= maximumEventCount) {
+                                result.add(entry.getKey());
+                            }
                         }
-                        j++;
                     }
                 }
             }
+
             return result;
         }
     }
 
-    private Condition getEventCondition(Condition condition, Map<String, Object> context) {
+    protected static Condition getEventCondition(Condition condition, Map<String, Object> context, String profileId,
+                                                 DefinitionsService definitionsService, ScriptExecutor scriptExecutor) {
         Condition eventCondition;
         try {
             eventCondition = (Condition) condition.getParameter("eventCondition");
@@ -198,36 +221,37 @@ public class PastEventConditionESQueryBuilder implements ConditionESQueryBuilder
 
         l.add(ConditionContextHelper.getContextualCondition(eventCondition, context, scriptExecutor));
 
+        if (profileId != null) {
+            Condition profileCondition = new Condition();
+            profileCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
+            profileCondition.setParameter("propertyName", "profileId");
+            profileCondition.setParameter("comparisonOperator", "equals");
+            profileCondition.setParameter("propertyValue", profileId);
+            l.add(profileCondition);
+        }
+
         Integer numberOfDays = (Integer) condition.getParameter("numberOfDays");
         String fromDate = (String) condition.getParameter("fromDate");
         String toDate = (String) condition.getParameter("toDate");
 
         if (numberOfDays != null) {
-            Condition numberOfDaysCondition = new Condition();
-            numberOfDaysCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
-            numberOfDaysCondition.setParameter("propertyName", "timeStamp");
-            numberOfDaysCondition.setParameter("comparisonOperator", "greaterThan");
-            numberOfDaysCondition.setParameter("propertyValueDateExpr", "now-" + numberOfDays + "d");
-            l.add(numberOfDaysCondition);
+            l.add(getTimeStampCondition("greaterThan", "propertyValueDateExpr", "now-" + numberOfDays + "d", definitionsService));
         }
         if (fromDate != null)  {
-            Condition startDateCondition = new Condition();
-            startDateCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
-            startDateCondition.setParameter("propertyName", "timeStamp");
-            startDateCondition.setParameter("comparisonOperator", "greaterThanOrEqualTo");
-            startDateCondition.setParameter("propertyValueDate", fromDate);
-            l.add(startDateCondition);
+            l.add(getTimeStampCondition("greaterThanOrEqualTo", "propertyValueDate", fromDate, definitionsService));
         }
         if (toDate != null)  {
-            Condition endDateCondition = new Condition();
-            endDateCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
-            endDateCondition.setParameter("propertyName", "timeStamp");
-            endDateCondition.setParameter("comparisonOperator", "lessThanOrEqualTo");
-            endDateCondition.setParameter("propertyValueDate", toDate);
-            l.add(endDateCondition);
+            l.add(getTimeStampCondition("lessThanOrEqualTo", "propertyValueDate", toDate, definitionsService));
         }
-
         return andCondition;
     }
 
-}
+    private static Condition getTimeStampCondition(String operator, String propertyValueParameter, Object propertyValue, DefinitionsService definitionsService) {
+        Condition endDateCondition = new Condition();
+        endDateCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
+        endDateCondition.setParameter("propertyName", "timeStamp");
+        endDateCondition.setParameter("comparisonOperator", operator);
+        endDateCondition.setParameter(propertyValueParameter, propertyValue);
+        return endDateCondition;
+    }
+}
\ No newline at end of file
diff --git a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionEvaluator.java b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionEvaluator.java
index 5ca8e10..721d9a7 100644
--- a/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionEvaluator.java
+++ b/plugins/baseplugin/src/main/java/org/apache/unomi/plugins/baseplugin/conditions/PastEventConditionEvaluator.java
@@ -22,22 +22,17 @@ import org.apache.unomi.api.Item;
 import org.apache.unomi.api.Profile;
 import org.apache.unomi.api.conditions.Condition;
 import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.persistence.elasticsearch.conditions.ConditionContextHelper;
 import org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator;
 import org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluatorDispatcher;
 import org.apache.unomi.persistence.spi.PersistenceService;
 import org.apache.unomi.scripting.ScriptExecutor;
 
-import java.util.ArrayList;
-import java.util.List;
 import java.util.Map;
 
 public class PastEventConditionEvaluator implements ConditionEvaluator {
 
     private PersistenceService persistenceService;
-
     private DefinitionsService definitionsService;
-
     private ScriptExecutor scriptExecutor;
 
     public void setPersistenceService(PersistenceService persistenceService) {
@@ -54,11 +49,7 @@ public class PastEventConditionEvaluator implements ConditionEvaluator {
 
     @Override
     public boolean eval(Condition condition, Item item, Map<String, Object> context, ConditionEvaluatorDispatcher dispatcher) {
-
         final Map<String, Object> parameters = condition.getParameterValues();
-
-        Condition eventCondition = (Condition) parameters.get("eventCondition");
-
         long count;
 
         if (parameters.containsKey("generatedPropertyKey")) {
@@ -71,61 +62,18 @@ public class PastEventConditionEvaluator implements ConditionEvaluator {
             } else {
                 count = 0;
             }
-
         } else {
-            if (eventCondition == null) {
-                throw new IllegalArgumentException("No eventCondition");
-            }
-
-            List<Condition> l = new ArrayList<Condition>();
-            Condition andCondition = new Condition();
-            andCondition.setConditionType(definitionsService.getConditionType("booleanCondition"));
-            andCondition.setParameter("operator", "and");
-            andCondition.setParameter("subConditions", l);
-
-            l.add(ConditionContextHelper.getContextualCondition(eventCondition, context, scriptExecutor));
-
-            Condition profileCondition = new Condition();
-            profileCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
-            profileCondition.setParameter("propertyName", "profileId");
-            profileCondition.setParameter("comparisonOperator", "equals");
-            profileCondition.setParameter("propertyValue", item.getItemId());
-            l.add(profileCondition);
-
-            Integer numberOfDays = (Integer) condition.getParameter("numberOfDays");
-            String fromDate = (String) condition.getParameter("fromDate");
-            String toDate = (String) condition.getParameter("toDate");
-
-            if (numberOfDays != null) {
-                Condition numberOfDaysCondition = new Condition();
-                numberOfDaysCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
-                numberOfDaysCondition.setParameter("propertyName", "timeStamp");
-                numberOfDaysCondition.setParameter("comparisonOperator", "greaterThan");
-                numberOfDaysCondition.setParameter("propertyValueDateExpr", "now-" + numberOfDays + "d");
-                l.add(numberOfDaysCondition);
-            }
-            if (fromDate != null)  {
-                Condition startDateCondition = new Condition();
-                startDateCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
-                startDateCondition.setParameter("propertyName", "timeStamp");
-                startDateCondition.setParameter("comparisonOperator", "greaterThanOrEqualTo");
-                startDateCondition.setParameter("propertyValueDate", fromDate);
-                l.add(startDateCondition);
-            }
-            if (toDate != null)  {
-                Condition endDateCondition = new Condition();
-                endDateCondition.setConditionType(definitionsService.getConditionType("sessionPropertyCondition"));
-                endDateCondition.setParameter("propertyName", "timeStamp");
-                endDateCondition.setParameter("comparisonOperator", "lessThanOrEqualTo");
-                endDateCondition.setParameter("propertyValueDate", toDate);
-                l.add(endDateCondition);
-            }
-            count = persistenceService.queryCount(andCondition, Event.ITEM_TYPE);
+            // TODO see for deprecation, this should not happen anymore each past event condition should have a generatedPropertyKey
+            count = persistenceService.queryCount(PastEventConditionESQueryBuilder.getEventCondition(condition, context, item.getItemId(), definitionsService, scriptExecutor), Event.ITEM_TYPE);
         }
 
-        Integer minimumEventCount = parameters.get("minimumEventCount") == null  ? 0 : (Integer) parameters.get("minimumEventCount");
-        Integer maximumEventCount = parameters.get("maximumEventCount") == null  ? Integer.MAX_VALUE : (Integer) parameters.get("maximumEventCount");
-
-        return count > 0 && (count >= minimumEventCount && count <= maximumEventCount);
+        boolean eventsOccurred = PastEventConditionESQueryBuilder.getStrategyFromOperator((String) condition.getParameter("operator"));
+        if (eventsOccurred) {
+            int minimumEventCount = parameters.get("minimumEventCount") == null  ? 0 : (Integer) parameters.get("minimumEventCount");
+            int maximumEventCount = parameters.get("maximumEventCount") == null  ? Integer.MAX_VALUE : (Integer) parameters.get("maximumEventCount");
+            return count > 0 && (count >= minimumEventCount && count <= maximumEventCount);
+        } else {
+            return count == 0;
+        }
     }
 }
diff --git a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/IdsCondition.json b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/IdsCondition.json
new file mode 100644
index 0000000..530e7ff
--- /dev/null
+++ b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/IdsCondition.json
@@ -0,0 +1,23 @@
+{
+  "metadata": {
+    "id": "idsCondition",
+    "name": "idsCondition",
+    "description": "",
+    "systemTags": [],
+    "readOnly": true
+  },
+  "conditionEvaluator": "idsConditionEvaluator",
+  "queryBuilder": "idsConditionESQueryBuilder",
+  "parameters": [
+    {
+      "id": "ids",
+      "type": "string",
+      "multivalued": true
+    },
+    {
+      "id": "match",
+      "type": "boolean",
+      "multivalued": false
+    }
+  ]
+}
\ No newline at end of file
diff --git a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/pastEventCondition.json b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/pastEventCondition.json
index 6740a73..0d70acb 100644
--- a/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/pastEventCondition.json
+++ b/plugins/baseplugin/src/main/resources/META-INF/cxs/conditions/pastEventCondition.json
@@ -41,6 +41,11 @@
       "multivalued": false
     },
     {
+      "id": "operator",
+      "type": "string",
+      "multivalued": false
+    },
+    {
       "id": "eventCondition",
       "type": "Condition",
       "multivalued": false
diff --git a/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml b/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
index f2c3846..b80f250 100644
--- a/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
+++ b/plugins/baseplugin/src/main/resources/OSGI-INF/blueprint/blueprint.xml
@@ -60,6 +60,16 @@
     <service
             interface="org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBuilder">
         <service-properties>
+            <entry key="queryBuilderId" value="idsConditionESQueryBuilder"/>
+        </service-properties>
+        <bean class="org.apache.unomi.plugins.baseplugin.conditions.IdsConditionESQueryBuilder">
+            <property name="maximumIdsQueryCount" value="${es.maximumIdsQueryCount}"/>
+        </bean>
+    </service>
+
+    <service
+            interface="org.apache.unomi.persistence.elasticsearch.conditions.ConditionESQueryBuilder">
+        <service-properties>
             <entry key="queryBuilderId" value="booleanConditionESQueryBuilder"/>
         </service-properties>
         <bean class="org.apache.unomi.plugins.baseplugin.conditions.BooleanConditionESQueryBuilder"/>
@@ -132,6 +142,13 @@
 
     <service interface="org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator">
         <service-properties>
+            <entry key="conditionEvaluatorId" value="idsConditionEvaluator"/>
+        </service-properties>
+        <bean class="org.apache.unomi.plugins.baseplugin.conditions.IdsConditionEvaluator"/>
+    </service>
+
+    <service interface="org.apache.unomi.persistence.elasticsearch.conditions.ConditionEvaluator">
+        <service-properties>
             <entry key="conditionEvaluatorId" value="notConditionEvaluator"/>
         </service-properties>
         <bean class="org.apache.unomi.plugins.baseplugin.conditions.NotConditionEvaluator"/>