Skip to content

Example: Online Ad Targeting

This example models an online advertising system where each advertiser campaign is a Boolean targeting rule. When a user visits a page, Mustang finds all matching campaigns in milliseconds — even across millions of rules.


Scenario

An ad platform has campaigns with rules like:

Campaign Rule
cricket-fans-india Country ∈ {IN} AND interest ∈ {cricket, sports}
tech-apac-premium (Country ∈ {IN,SG,JP} AND tier ∈ {premium}) OR (Country ∈ {AU} AND age ≥ 25)
global-exclude-ios Country ∈ {IN,SG,US,GB} AND platform ∉ {ios}
young-android Age ∈ [18,25] AND platform ∈

A user visits with profile: {country: "IN", age: 23, platform: "android", interest: "cricket", tier: "free"}.


Step 1: Build the engine and index all campaigns

import com.fasterxml.jackson.databind.ObjectMapper;
import com.phonepe.mustang.MustangEngine;
import com.phonepe.mustang.criteria.Criteria;
import com.phonepe.mustang.criteria.impl.DNFCriteria;
import com.phonepe.mustang.composition.impl.Conjunction;
import com.phonepe.mustang.predicate.impl.IncludedPredicate;
import com.phonepe.mustang.predicate.impl.ExcludedPredicate;
import com.phonepe.mustang.detail.impl.EqualityDetail;
import com.phonepe.mustang.detail.impl.RangeDetail;
import com.google.common.collect.Sets;

ObjectMapper mapper = new ObjectMapper();
MustangEngine engine = MustangEngine.builder().mapper(mapper).build();

// Campaign 1: cricket fans in India
Criteria campaign1 = DNFCriteria.builder()
    .id("cricket-fans-india")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.country")
            .weight(3L)
            .detail(EqualityDetail.builder().values(Sets.newHashSet("IN")).build())
            .build())
        .predicate(IncludedPredicate.builder()
            .lhs("$.interest")
            .weight(10L)
            .detail(EqualityDetail.builder().values(Sets.newHashSet("cricket", "sports")).build())
            .build())
        .build())
    .build();

// Campaign 2: tech-savvy APAC users, premium tier or Australia + age
Criteria campaign2 = DNFCriteria.builder()
    .id("tech-apac-premium")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.country")
            .weight(2L)
            .detail(EqualityDetail.builder().values(Sets.newHashSet("IN","SG","JP")).build())
            .build())
        .predicate(IncludedPredicate.builder()
            .lhs("$.tier")
            .weight(5L)
            .detail(EqualityDetail.builder().values(Sets.newHashSet("premium")).build())
            .build())
        .build())
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.country")
            .detail(EqualityDetail.builder().values(Sets.newHashSet("AU")).build())
            .build())
        .predicate(IncludedPredicate.builder()
            .lhs("$.age")
            .detail(RangeDetail.builder().lowerBound(25).includeLowerBound(true).build())
            .build())
        .build())
    .build();

// Campaign 3: broad reach, exclude iOS
Criteria campaign3 = DNFCriteria.builder()
    .id("global-exclude-ios")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.country")
            .weight(1L)
            .detail(EqualityDetail.builder().values(Sets.newHashSet("IN","SG","US","GB")).build())
            .build())
        .predicate(ExcludedPredicate.builder()
            .lhs("$.platform")
            .detail(EqualityDetail.builder().values(Sets.newHashSet("ios")).build())
            .build())
        .build())
    .build();

// Campaign 4: young Android users
Criteria campaign4 = DNFCriteria.builder()
    .id("young-android")
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.age")
            .weight(4L)
            .detail(RangeDetail.builder()
                .lowerBound(18).includeLowerBound(true)
                .upperBound(25).includeUpperBound(true)
                .build())
            .build())
        .predicate(IncludedPredicate.builder()
            .lhs("$.platform")
            .weight(3L)
            .detail(EqualityDetail.builder().values(Sets.newHashSet("android")).build())
            .build())
        .build())
    .build();

engine.add("ads", List.of(campaign1, campaign2, campaign3, campaign4));

Step 2: Search for a user event

import com.fasterxml.jackson.databind.JsonNode;
import com.phonepe.mustang.common.RequestContext;

JsonNode userEvent = mapper.readTree("""
    {
      "country": "IN",
      "age": 23,
      "platform": "android",
      "interest": "cricket",
      "tier": "free"
    }
    """);

RequestContext ctx = RequestContext.builder().node(userEvent).build();

// All matching campaigns
Set<String> matches = engine.search("ads", ctx);
// → ["cricket-fans-india", "global-exclude-ios", "young-android"]
// Note: "tech-apac-premium" does NOT match because tier="free" and country≠AU

Step 3: Top-N ranked selection

In display advertising you can only show a limited number of ads. Use top-N to get the best-scoring campaigns:

// Show only top 2 campaigns (by relevance score)
Set<String> top2 = engine.search("ads", ctx, 2);
// Scores:
//   cricket-fans-india:  3*1 + 10*1 = 13
//   global-exclude-ios:  1*1        = 1
//   young-android:       4*1 + 3*1  = 7
// Top 2 → ["cricket-fans-india", "young-android"]

Step 4: Debug why a campaign matched or didn't

DebugResult debug = engine.debug(campaign2, ctx);
System.out.println(debug.isResult());   // false

// Per-conjunction breakdown:
debug.getCompositionDebugResults().forEach(conjResult -> {
    System.out.println("Conjunction matched: " + conjResult.isResult());
    conjResult.getPredicateDebugResults().forEach(p ->
        System.out.println("  " + p.getLhs() + " → " + p.isResult())
    );
});
// Output:
//   Conjunction matched: false
//     $.country → true   (IN is in {IN,SG,JP})
//     $.tier    → false  (free is not in {premium})
//   Conjunction matched: false
//     $.country → false  (IN is not in {AU})
//     $.age     → true   (23 >= 25 is false... actually false)

Step 5: Live campaign updates

// Advertiser updates their campaign targeting
Criteria updatedCampaign1 = DNFCriteria.builder()
    .id("cricket-fans-india")          // same ID → replaces existing
    .conjunction(Conjunction.builder()
        .predicate(IncludedPredicate.builder()
            .lhs("$.country")
            .weight(3L)
            .detail(EqualityDetail.builder().values(Sets.newHashSet("IN", "PK")).build()) // added PK
            .build())
        .predicate(IncludedPredicate.builder()
            .lhs("$.interest")
            .weight(10L)
            .detail(EqualityDetail.builder().values(Sets.newHashSet("cricket", "sports")).build())
            .build())
        .build())
    .build();

engine.update("ads", updatedCampaign1);

// Pause campaign
engine.delete("ads", campaign4);

Step 6: Nightly consistency check

engine.ratify("ads");

// Later...
RatificationResult result = engine.getRatificationResult("ads");
if (!Boolean.TRUE.equals(result.getStatus())) {
    alertOncall("Ad index anomaly detected: " + result.getAnamolyDetails());
}