Skip to content

Predicates & Details

A predicate is the atomic unit of a criteria. It tests whether a single attribute in the incoming event satisfies a condition.

There are two predicate types:

Type Semantics Java class
IncludedPredicate Attribute must satisfy the detail condition (∈) IncludedPredicate
ExcludedPredicate Attribute must NOT satisfy the detail condition (∉) ExcludedPredicate

Every predicate has:

  • lhs — a JSONPath expression pointing to the attribute in the event (e.g., $.country, $.user.age)
  • detail — the caveat that defines what "satisfying the condition" means
  • weight (optional) — a numeric weight used for scoring
  • preOperations (optional) — a list of transformations applied to the value before evaluation

Detail types

EqualityDetail — exact value match

Tests whether the attribute value is a member of a given set.

EqualityDetail.builder()
    .values(Sets.newHashSet("IN", "SG", "US"))
    .build()

Supports String, Number, Boolean.

Example — country must be India or Singapore:

IncludedPredicate.builder()
    .lhs("$.country")
    .detail(EqualityDetail.builder()
        .values(Sets.newHashSet("IN", "SG"))
        .build())
    .build()

Shorthand — you can pass values directly without wrapping in EqualityDetail:

IncludedPredicate.builder()
    .lhs("$.country")
    .values(Sets.newHashSet("IN", "SG"))   // implicit EqualityDetail
    .build()

RangeDetail — numeric range

Tests whether the attribute value falls within a numeric range. Both bounds are optional — omitting a bound means open (unbounded).

RangeDetail.builder()
    .lowerBound(18)
    .includeLowerBound(true)   // >=
    .upperBound(35)
    .includeUpperBound(true)   // <=
    .build()
Scenario Configuration
x >= 18 lowerBound(18).includeLowerBound(true)
x > 18 lowerBound(18).includeLowerBound(false)
x < 100 upperBound(100).includeUpperBound(false)
x <= 100 upperBound(100).includeUpperBound(true)
18 <= x < 35 both bounds, includeLowerBound(true), includeUpperBound(false)
unbounded above omit upperBound (defaults to Double.MAX_VALUE)
unbounded below omit lowerBound (defaults to Double.MIN_VALUE)

Example — age between 18 and 35 inclusive:

IncludedPredicate.builder()
    .lhs("$.age")
    .detail(RangeDetail.builder()
        .lowerBound(18).includeLowerBound(true)
        .upperBound(35).includeUpperBound(true)
        .build())
    .build()

RegexDetail — regular expression

Tests the attribute value against a Java regular expression.

RegexDetail.builder()
    .regex("^[A-Z]{2}\\d+$")
    .build()

Only applies to String values. Uses Java's java.util.regex engine with the rgxgen library for expression validation.

Example — phone number starting with +91:

IncludedPredicate.builder()
    .lhs("$.phone")
    .detail(RegexDetail.builder()
        .regex("^\\+91\\d{10}$")
        .build())
    .build()

VersioningDetail — semantic version comparison

Compares the attribute value as a semantic version string against a base version, using Maven's ComparableVersion — which handles formats like 1.2.3, 1.2.3-alpha, 2.0.0.RC1.

VersioningDetail.builder()
    .check(CheckType.ABOVE)       // ABOVE | BELOW | EQUAL
    .baseVersion("3.2.0")
    .excludeBase(false)           // if true: strictly above/below (not equal to base)
    .build()
CheckType excludeBase=false excludeBase=true
ABOVE version >= baseVersion version > baseVersion
BELOW version <= baseVersion version < baseVersion
EQUAL version == baseVersion (same)

Example — app version must be 3.2.0 or higher:

IncludedPredicate.builder()
    .lhs("$.appVersion")
    .detail(VersioningDetail.builder()
        .check(CheckType.ABOVE)
        .baseVersion("3.2.0")
        .build())
    .build()

SubSetDetail — subset check

Tests whether the attribute (a collection) is a subset of the given set — i.e., every element in the attribute appears in the detail's value set.

SubSetDetail.builder()
    .values(Sets.newHashSet("READ", "WRITE", "DELETE"))
    .build()

Example — user permissions are a subset of {READ, WRITE}:

IncludedPredicate.builder()
    .lhs("$.permissions")
    .detail(SubSetDetail.builder()
        .values(Sets.newHashSet("READ", "WRITE"))
        .build())
    .build()

SuperSetDetail — superset check

Tests whether the attribute (a collection) is a superset of the given set — i.e., every element in the detail's value set appears in the attribute.

SuperSetDetail.builder()
    .values(Sets.newHashSet("ADMIN"))
    .build()

Example — user must have at least the ADMIN role:

IncludedPredicate.builder()
    .lhs("$.roles")
    .detail(SuperSetDetail.builder()
        .values(Sets.newHashSet("ADMIN"))
        .build())
    .build()

EqualSetDetail — exact set equality

Tests whether the attribute collection is exactly equal to the given set (same elements, no more, no less).

EqualSetDetail.builder()
    .values(Sets.newHashSet("READ", "WRITE"))
    .build()

IntersectionDetail — non-empty intersection check

Tests whether the attribute collection and the detail's value set share at least one common element — i.e., their intersection is non-empty. Returns true if any element matches, false otherwise.

IntersectionDetail.builder()
    .values(Sets.newHashSet("ADMIN", "EDITOR"))
    .build()

Example — user must hold at least one privileged role:

IncludedPredicate.builder()
    .lhs("$.roles")
    .detail(IntersectionDetail.builder()
        .values(Sets.newHashSet("ADMIN", "EDITOR"))
        .build())
    .build()

ExistenceDetail — attribute must be present

Tests that the attribute path exists (is non-null) in the event.

ExistenceDetail.builder().build()

Example — event must have a userId field:

IncludedPredicate.builder()
    .lhs("$.userId")
    .detail(ExistenceDetail.builder().build())
    .build()

NonExistenceDetail — attribute must be absent

Tests that the attribute path does not exist (is null or missing) in the event.

NonExistenceDetail.builder().build()

Caveat support matrix

Caveat String Number Boolean Collection
EQUALITY ✓ ✓ ✓ —
RANGE — ✓ — —
REGEX ✓ — — —
VERSIONING ✓ — — —
SUBSET — — — ✓
SUPERSET — — — ✓
EQUALSET — — — ✓
INTERSECTION — — — ✓
EXISTENCE ✓ ✓ ✓ ✓
NON_EXISTENCE ✓ ✓ ✓ ✓

ExcludedPredicate — negation

Any detail can be negated by wrapping it in an ExcludedPredicate. Evaluation is !detail.validate(value).

// platform must NOT be in {ios, windows}
ExcludedPredicate.builder()
    .lhs("$.platform")
    .detail(EqualityDetail.builder()
        .values(Sets.newHashSet("ios", "windows"))
        .build())
    .build()

Scoring for exclusions

Exclusion predicates contribute 0 to the score (not NO_MATCH), since their satisfaction is implicit. Only inclusion predicates with explicit weights contribute to the relevance score.


Combining predicates in compositions

Conjunction (AND)

Conjunction.builder()
    .predicate(p1)
    .predicate(p2)
    .predicate(p3)
    .build()
// satisfied iff p1 AND p2 AND p3

Disjunction (OR)

Disjunction.builder()
    .predicate(p1)
    .predicate(p2)
    .build()
// satisfied iff p1 OR p2

UNFCriteria — unrestricted nesting

For complex compositions that go beyond pure DNF or CNF, UNFCriteria allows nesting arbitrary criteria trees:

import com.phonepe.mustang.criteria.impl.UNFCriteria;
import com.phonepe.mustang.composition.CompositionType;

UNFCriteria complex = UNFCriteria.builder()
    .id("complex-rule")
    .type(CompositionType.AND)     // AND or OR at this level
    .criteria(subCriteria1)        // nested criteria
    .criteria(subCriteria2)
    .predicate(standaloneP1)       // predicates at this level
    .build();

UNF is not indexed

UNFCriteria is evaluated by engine.evaluate() or engine.scan(). It cannot be added to an inverted index via engine.add() — only DNFCriteria and CNFCriteria are indexable.