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:51 UTC

[unomi] branch handleNegativePastEventCondition created (now a734dc7)

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

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


      at a734dc7  UNOMI-550: handle negative past event condition and code cleaning

This branch includes the following new commits:

     new a734dc7  UNOMI-550: handle negative past event condition and code cleaning

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


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

Posted by jk...@apache.org.
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"/>