adservice: find relevant ads by category (#61)

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:
sebright 2018-10-01 22:44:56 -07:00 committed by Ahmet Alp Balkan
parent 86c8c06cc1
commit dc7effd601
11 changed files with 1532 additions and 925 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();

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -64,6 +64,10 @@ message Product {
string description = 3;
string picture = 4;
Money price_usd = 5;
// Categories such as "vintage" or "gardening" that can be used to look up
// other related products.
repeated string categories = 6;
}
message ListProductsResponse {
@ -218,18 +222,18 @@ message PlaceOrderResponse {
OrderResult order = 1;
}
// ------------Ads service------------------
// ------------Ad service------------------
service AdsService {
rpc GetAds(AdsRequest) returns (AdsResponse) {}
service AdService {
rpc GetAds(AdRequest) returns (AdResponse) {}
}
message AdsRequest {
message AdRequest {
// List of important key words from the current page describing the context.
repeated string context_keys = 1;
}
message AdsResponse {
message AdResponse {
repeated Ad ads = 1;
}

View file

@ -26,17 +26,6 @@
revision = "37aa2801fbf0205003e15636096ebf0373510288"
version = "v0.5.0"
[[projects]]
branch = "master"
digest = "1:3ef905a7059a17712b7b27315692992f84f356e828d38f6ff0624e04103ec675"
name = "github.com/GoogleCloudPlatform/microservices-demo"
packages = [
"src/frontend/genproto",
"src/frontend/money",
]
pruneopts = "UT"
revision = "6d969441585ade8c91c235115c7cdb12ac61354f"
[[projects]]
digest = "1:72856926f8208767b837bf51e3373f49139f61889b67dc7fd3c2a0fd711e3f7a"
name = "github.com/golang/protobuf"

View file

@ -33,10 +33,6 @@
name = "contrib.go.opencensus.io/exporter/stackdriver"
version = "0.5.0"
[[constraint]]
branch = "master"
name = "github.com/GoogleCloudPlatform/microservices-demo"
[[constraint]]
name = "github.com/golang/protobuf"
version = "1.2.0"

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

@ -64,6 +64,10 @@ message Product {
string description = 3;
string picture = 4;
Money price_usd = 5;
// Categories such as "vintage" or "gardening" that can be used to look up
// other related products.
repeated string categories = 6;
}
message ListProductsResponse {
@ -218,18 +222,18 @@ message PlaceOrderResponse {
OrderResult order = 1;
}
// ------------Ads service------------------
// ------------Ad service------------------
service AdsService {
rpc GetAds(AdsRequest) returns (AdsResponse) {}
service AdService {
rpc GetAds(AdRequest) returns (AdResponse) {}
}
message AdsRequest {
message AdRequest {
// List of important key words from the current page describing the context.
repeated string context_keys = 1;
}
message AdsResponse {
message AdResponse {
repeated Ad ads = 1;
}

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"]
}
]
}

File diff suppressed because it is too large Load diff