adservice: find relevant ads by category

The ad service now returns ads matching the categories of the product that is
currently displayed.  Changes in this commit:

- List all products' categories in products.json.

- Pass the current product's categories from the frontend to the ad service when
  looking up ads.

- Store a statically initialized multimap from product category to ad in the ad
  service.

- Return all ads matching the given categories when handling an ads request.
  The ad service continues to return random ads when no categories are given or
  no ads match the categories.
This commit is contained in:
Kristen Kozak 2018-09-29 14:21:46 -07:00
parent 503cac7f86
commit b27a7eb36a
4 changed files with 65 additions and 42 deletions

View file

@ -17,6 +17,8 @@
package hipstershop;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.Iterables;
import hipstershop.Demo.Ad;
import hipstershop.Demo.AdRequest;
import hipstershop.Demo.AdResponse;
@ -42,6 +44,7 @@ import io.opencensus.trace.Tracer;
import io.opencensus.trace.Tracing;
import io.opencensus.trace.samplers.Samplers;
import java.io.IOException;
import java.util.Collection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@ -109,8 +112,8 @@ public class AdService {
try (Scope scope = spanBuilder.startScopedSpan()) {
Span span = tracer.getCurrentSpan();
span.putAttribute("method", AttributeValue.stringAttributeValue("getAds"));
List<Ad> ads = new ArrayList<>();
logger.info("received ad request (context_words=" + req.getContextKeysCount() + ")");
List<Ad> allAds = new ArrayList<>();
logger.info("received ad request (context_words=" + req.getContextKeysList() + ")");
if (req.getContextKeysCount() > 0) {
span.addAnnotation(
"Constructing Ads using context",
@ -120,21 +123,19 @@ public class AdService {
"Context Keys length",
AttributeValue.longAttributeValue(req.getContextKeysCount())));
for (int i = 0; i < req.getContextKeysCount(); i++) {
Ad ad = service.getAdsByKey(req.getContextKeys(i));
if (ad != null) {
ads.add(ad);
}
Collection<Ad> ads = service.getAdsByCategory(req.getContextKeys(i));
allAds.addAll(ads);
}
} else {
span.addAnnotation("No Context provided. Constructing random Ads.");
ads = service.getDefaultAds();
allAds = service.getRandomAds();
}
if (ads.isEmpty()) {
// Serve default ads.
if (allAds.isEmpty()) {
// Serve random ads.
span.addAnnotation("No Ads found based on context. Constructing random Ads.");
ads = service.getDefaultAds();
allAds = service.getRandomAds();
}
AdResponse reply = AdResponse.newBuilder().addAllAds(ads).build();
AdResponse reply = AdResponse.newBuilder().addAllAds(allAds).build();
responseObserver.onNext(reply);
responseObserver.onCompleted();
} catch (StatusRuntimeException e) {
@ -144,18 +145,19 @@ public class AdService {
}
}
static final HashMap<String, Ad> cacheMap = new HashMap<String, Ad>();
static final ImmutableListMultimap<String, Ad> adsMap = createAdsMap();
Ad getAdsByKey(String key) {
return cacheMap.get(key);
Collection<Ad> getAdsByCategory(String category) {
return adsMap.get(category);
}
private static final Random random = new Random();
public List<Ad> getDefaultAds() {
public List<Ad> getRandomAds() {
List<Ad> ads = new ArrayList<>(MAX_ADS_TO_SERVE);
Object[] keys = cacheMap.keySet().toArray();
Collection<Ad> allAds = adsMap.values();
for (int i=0; i<MAX_ADS_TO_SERVE; i++) {
ads.add(cacheMap.get(keys[new Random().nextInt(keys.length)]));
ads.add(Iterables.get(allAds, random.nextInt(allAds.size())));
}
return ads;
}
@ -171,14 +173,28 @@ public class AdService {
}
}
static void initializeAds() {
cacheMap.put("camera", Ad.newBuilder().setRedirectUrl( "/product/2ZYFJ3GM2N")
.setText("Film camera for sale. 50% off.").build());
cacheMap.put("bike", Ad.newBuilder().setRedirectUrl("/product/9SIQT8TOJO")
.setText("City Bike for sale. 10% off.").build());
cacheMap.put("kitchen", Ad.newBuilder().setRedirectUrl("/product/1YMWWN1N4O")
.setText("Home Barista kitchen kit for sale. Buy one, get second kit for free").build());
logger.info("Default Ads initialized");
static ImmutableListMultimap<String, Ad> createAdsMap() {
Ad camera = Ad.newBuilder().setRedirectUrl("/product/2ZYFJ3GM2N")
.setText("Film camera for sale. 50% off.").build();
Ad lens = Ad.newBuilder().setRedirectUrl("/product/66VCHSJNUP")
.setText("Vintage camera lens for sale. 20% off.").build();
Ad recordPlayer = Ad.newBuilder().setRedirectUrl("/product/0PUK6V6EV0")
.setText("Vintage record player for sale. 30% off.").build();
Ad bike = Ad.newBuilder().setRedirectUrl("/product/9SIQT8TOJO")
.setText("City Bike for sale. 10% off.").build();
Ad baristaKit = Ad.newBuilder().setRedirectUrl("/product/1YMWWN1N4O")
.setText("Home Barista kitchen kit for sale. Buy one, get second kit for free").build();
Ad airPlant = Ad.newBuilder().setRedirectUrl("/product/6E92ZMYYFZ")
.setText("Air plants for sale. Buy two, get third one for free").build();
Ad terrarium = Ad.newBuilder().setRedirectUrl("/product/L9ECAV7KIM")
.setText("Terrarium for sale. Buy one, get second one for free").build();
return ImmutableListMultimap.<String, Ad>builder()
.putAll("photography", camera, lens)
.putAll("vintage", camera, lens, recordPlayer)
.put("cycling", bike)
.put("cookware", baristaKit)
.putAll("gardening", airPlant, terrarium)
.build();
}
public static void initStackdriver() {
@ -222,8 +238,6 @@ public class AdService {
public static void main(String[] args) throws IOException, InterruptedException {
// Add final keyword to pass checkStyle.
initializeAds();
new Thread( new Runnable() {
public void run(){
initStackdriver();

View file

@ -80,7 +80,7 @@ func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) {
"products": ps,
"cart_size": len(cart),
"banner_color": os.Getenv("BANNER_COLOR"), // illustrates canary deployments
"ad": fe.chooseAd(r.Context(), log),
"ad": fe.chooseAd(r.Context(), []string{}, log),
}); err != nil {
log.Error(err)
}
@ -133,7 +133,7 @@ func (fe *frontendServer) productHandler(w http.ResponseWriter, r *http.Request)
if err := templates.ExecuteTemplate(w, "product", map[string]interface{}{
"session_id": sessionID(r),
"request_id": r.Context().Value(ctxKeyRequestID{}),
"ad": fe.chooseAd(r.Context(), log),
"ad": fe.chooseAd(r.Context(), p.Categories, log),
"user_currency": currentCurrency(r),
"currencies": currencies,
"product": product,
@ -346,8 +346,8 @@ func (fe *frontendServer) setCurrencyHandler(w http.ResponseWriter, r *http.Requ
// chooseAd queries for advertisements available and randomly chooses one, if
// available. It ignores the error retrieving the ad since it is not critical.
func (fe *frontendServer) chooseAd(ctx context.Context, log logrus.FieldLogger) *pb.Ad {
ads, err := fe.getAd(ctx)
func (fe *frontendServer) chooseAd(ctx context.Context, ctxKeys []string, log logrus.FieldLogger) *pb.Ad {
ads, err := fe.getAd(ctx, ctxKeys)
if err != nil {
log.WithField("error", err).Warn("failed to retrieve ads")
return nil

View file

@ -116,12 +116,12 @@ func (fe *frontendServer) getRecommendations(ctx context.Context, userID string,
return out, err
}
func (fe *frontendServer) getAd(ctx context.Context) ([]*pb.Ad, error) {
func (fe *frontendServer) getAd(ctx context.Context, ctxKeys []string) ([]*pb.Ad, error) {
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*100)
defer cancel()
resp, err := pb.NewAdServiceClient(fe.adSvcConn).GetAds(ctx, &pb.AdRequest{
ContextKeys: nil,
ContextKeys: ctxKeys,
})
return resp.GetAds(), errors.Wrap(err, "failed to get ads")
}

View file

@ -9,7 +9,8 @@
"currencyCode": "USD",
"units": 67,
"nanos": 990000000
}
},
"categories": ["vintage"]
},
{
"id": "66VCHSJNUP",
@ -20,7 +21,8 @@
"currencyCode": "USD",
"units": 12,
"nanos": 490000000
}
},
"categories": ["photography", "vintage"]
},
{
"id": "1YMWWN1N4O",
@ -30,7 +32,8 @@
"priceUsd": {
"currencyCode": "USD",
"units": 124
}
},
"categories": ["cookware"]
},
{
"id": "L9ECAV7KIM",
@ -41,7 +44,8 @@
"currencyCode": "USD",
"units": 36,
"nanos": 450000000
}
},
"categories": ["gardening"]
},
{
"id": "2ZYFJ3GM2N",
@ -51,7 +55,8 @@
"priceUsd": {
"currencyCode": "USD",
"units": 2245
}
},
"categories": ["photography", "vintage"]
},
{
"id": "0PUK6V6EV0",
@ -62,7 +67,8 @@
"currencyCode": "USD",
"units": 65,
"nanos": 500000000
}
},
"categories": ["music", "vintage"]
},
{
"id": "LS4PSXUNUM",
@ -73,7 +79,8 @@
"currencyCode": "USD",
"units": 24,
"nanos": 330000000
}
},
"categories": ["cookware"]
},
{
"id": "9SIQT8TOJO",
@ -84,7 +91,8 @@
"currencyCode": "USD",
"units": 789,
"nanos": 500000000
}
},
"categories": ["cycling"]
},
{
"id": "6E92ZMYYFZ",
@ -95,7 +103,8 @@
"currencyCode": "USD",
"units": 12,
"nanos": 300000000
}
},
"categories": ["gardening"]
}
]
}