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" meansweight(optional) — a numeric weight used for scoringpreOperations(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()
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 |
— | — | — | ✓ |
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.