From b27a7eb36a2b39754051ae81e4f6c71ca14b98d3 Mon Sep 17 00:00:00 2001 From: Kristen Kozak Date: Sat, 29 Sep 2018 14:21:46 -0700 Subject: [PATCH] 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. --- .../src/main/java/hipstershop/AdService.java | 68 +++++++++++-------- src/frontend/handlers.go | 8 +-- src/frontend/rpc.go | 4 +- src/productcatalogservice/products.json | 27 +++++--- 4 files changed, 65 insertions(+), 42 deletions(-) diff --git a/src/adservice/src/main/java/hipstershop/AdService.java b/src/adservice/src/main/java/hipstershop/AdService.java index 75ce76f..dfb1e40 100644 --- a/src/adservice/src/main/java/hipstershop/AdService.java +++ b/src/adservice/src/main/java/hipstershop/AdService.java @@ -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 ads = new ArrayList<>(); - logger.info("received ad request (context_words=" + req.getContextKeysCount() + ")"); + List 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 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 cacheMap = new HashMap(); + static final ImmutableListMultimap adsMap = createAdsMap(); - Ad getAdsByKey(String key) { - return cacheMap.get(key); + Collection getAdsByCategory(String category) { + return adsMap.get(category); } + private static final Random random = new Random(); - public List getDefaultAds() { + public List getRandomAds() { List ads = new ArrayList<>(MAX_ADS_TO_SERVE); - Object[] keys = cacheMap.keySet().toArray(); + Collection allAds = adsMap.values(); for (int i=0; i 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.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(); diff --git a/src/frontend/handlers.go b/src/frontend/handlers.go index 10bc2ff..d7e8765 100644 --- a/src/frontend/handlers.go +++ b/src/frontend/handlers.go @@ -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 diff --git a/src/frontend/rpc.go b/src/frontend/rpc.go index 05c99b2..a1dd313 100644 --- a/src/frontend/rpc.go +++ b/src/frontend/rpc.go @@ -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") } diff --git a/src/productcatalogservice/products.json b/src/productcatalogservice/products.json index be0825b..f41b2d9 100644 --- a/src/productcatalogservice/products.json +++ b/src/productcatalogservice/products.json @@ -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"] } ] }