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:
parent
86c8c06cc1
commit
dc7effd601
11 changed files with 1532 additions and 925 deletions
|
@ -17,6 +17,8 @@
|
||||||
package hipstershop;
|
package hipstershop;
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
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.Ad;
|
||||||
import hipstershop.Demo.AdRequest;
|
import hipstershop.Demo.AdRequest;
|
||||||
import hipstershop.Demo.AdResponse;
|
import hipstershop.Demo.AdResponse;
|
||||||
|
@ -42,6 +44,7 @@ import io.opencensus.trace.Tracer;
|
||||||
import io.opencensus.trace.Tracing;
|
import io.opencensus.trace.Tracing;
|
||||||
import io.opencensus.trace.samplers.Samplers;
|
import io.opencensus.trace.samplers.Samplers;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
@ -109,8 +112,8 @@ public class AdService {
|
||||||
try (Scope scope = spanBuilder.startScopedSpan()) {
|
try (Scope scope = spanBuilder.startScopedSpan()) {
|
||||||
Span span = tracer.getCurrentSpan();
|
Span span = tracer.getCurrentSpan();
|
||||||
span.putAttribute("method", AttributeValue.stringAttributeValue("getAds"));
|
span.putAttribute("method", AttributeValue.stringAttributeValue("getAds"));
|
||||||
List<Ad> ads = new ArrayList<>();
|
List<Ad> allAds = new ArrayList<>();
|
||||||
logger.info("received ad request (context_words=" + req.getContextKeysCount() + ")");
|
logger.info("received ad request (context_words=" + req.getContextKeysList() + ")");
|
||||||
if (req.getContextKeysCount() > 0) {
|
if (req.getContextKeysCount() > 0) {
|
||||||
span.addAnnotation(
|
span.addAnnotation(
|
||||||
"Constructing Ads using context",
|
"Constructing Ads using context",
|
||||||
|
@ -120,21 +123,19 @@ public class AdService {
|
||||||
"Context Keys length",
|
"Context Keys length",
|
||||||
AttributeValue.longAttributeValue(req.getContextKeysCount())));
|
AttributeValue.longAttributeValue(req.getContextKeysCount())));
|
||||||
for (int i = 0; i < req.getContextKeysCount(); i++) {
|
for (int i = 0; i < req.getContextKeysCount(); i++) {
|
||||||
Ad ad = service.getAdsByKey(req.getContextKeys(i));
|
Collection<Ad> ads = service.getAdsByCategory(req.getContextKeys(i));
|
||||||
if (ad != null) {
|
allAds.addAll(ads);
|
||||||
ads.add(ad);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
span.addAnnotation("No Context provided. Constructing random Ads.");
|
span.addAnnotation("No Context provided. Constructing random Ads.");
|
||||||
ads = service.getDefaultAds();
|
allAds = service.getRandomAds();
|
||||||
}
|
}
|
||||||
if (ads.isEmpty()) {
|
if (allAds.isEmpty()) {
|
||||||
// Serve default ads.
|
// Serve random ads.
|
||||||
span.addAnnotation("No Ads found based on context. Constructing 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.onNext(reply);
|
||||||
responseObserver.onCompleted();
|
responseObserver.onCompleted();
|
||||||
} catch (StatusRuntimeException e) {
|
} 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) {
|
Collection<Ad> getAdsByCategory(String category) {
|
||||||
return cacheMap.get(key);
|
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);
|
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++) {
|
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;
|
return ads;
|
||||||
}
|
}
|
||||||
|
@ -171,14 +173,28 @@ public class AdService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void initializeAds() {
|
static ImmutableListMultimap<String, Ad> createAdsMap() {
|
||||||
cacheMap.put("camera", Ad.newBuilder().setRedirectUrl( "/product/2ZYFJ3GM2N")
|
Ad camera = Ad.newBuilder().setRedirectUrl("/product/2ZYFJ3GM2N")
|
||||||
.setText("Film camera for sale. 50% off.").build());
|
.setText("Film camera for sale. 50% off.").build();
|
||||||
cacheMap.put("bike", Ad.newBuilder().setRedirectUrl("/product/9SIQT8TOJO")
|
Ad lens = Ad.newBuilder().setRedirectUrl("/product/66VCHSJNUP")
|
||||||
.setText("City Bike for sale. 10% off.").build());
|
.setText("Vintage camera lens for sale. 20% off.").build();
|
||||||
cacheMap.put("kitchen", Ad.newBuilder().setRedirectUrl("/product/1YMWWN1N4O")
|
Ad recordPlayer = Ad.newBuilder().setRedirectUrl("/product/0PUK6V6EV0")
|
||||||
.setText("Home Barista kitchen kit for sale. Buy one, get second kit for free").build());
|
.setText("Vintage record player for sale. 30% off.").build();
|
||||||
logger.info("Default Ads initialized");
|
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() {
|
public static void initStackdriver() {
|
||||||
|
@ -222,8 +238,6 @@ public class AdService {
|
||||||
public static void main(String[] args) throws IOException, InterruptedException {
|
public static void main(String[] args) throws IOException, InterruptedException {
|
||||||
// Add final keyword to pass checkStyle.
|
// Add final keyword to pass checkStyle.
|
||||||
|
|
||||||
initializeAds();
|
|
||||||
|
|
||||||
new Thread( new Runnable() {
|
new Thread( new Runnable() {
|
||||||
public void run(){
|
public void run(){
|
||||||
initStackdriver();
|
initStackdriver();
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -64,6 +64,10 @@ message Product {
|
||||||
string description = 3;
|
string description = 3;
|
||||||
string picture = 4;
|
string picture = 4;
|
||||||
Money price_usd = 5;
|
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 {
|
message ListProductsResponse {
|
||||||
|
@ -218,18 +222,18 @@ message PlaceOrderResponse {
|
||||||
OrderResult order = 1;
|
OrderResult order = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------Ads service------------------
|
// ------------Ad service------------------
|
||||||
|
|
||||||
service AdsService {
|
service AdService {
|
||||||
rpc GetAds(AdsRequest) returns (AdsResponse) {}
|
rpc GetAds(AdRequest) returns (AdResponse) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdsRequest {
|
message AdRequest {
|
||||||
// List of important key words from the current page describing the context.
|
// List of important key words from the current page describing the context.
|
||||||
repeated string context_keys = 1;
|
repeated string context_keys = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdsResponse {
|
message AdResponse {
|
||||||
repeated Ad ads = 1;
|
repeated Ad ads = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
11
src/frontend/Gopkg.lock
generated
11
src/frontend/Gopkg.lock
generated
|
@ -26,17 +26,6 @@
|
||||||
revision = "37aa2801fbf0205003e15636096ebf0373510288"
|
revision = "37aa2801fbf0205003e15636096ebf0373510288"
|
||||||
version = "v0.5.0"
|
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]]
|
[[projects]]
|
||||||
digest = "1:72856926f8208767b837bf51e3373f49139f61889b67dc7fd3c2a0fd711e3f7a"
|
digest = "1:72856926f8208767b837bf51e3373f49139f61889b67dc7fd3c2a0fd711e3f7a"
|
||||||
name = "github.com/golang/protobuf"
|
name = "github.com/golang/protobuf"
|
||||||
|
|
|
@ -33,10 +33,6 @@
|
||||||
name = "contrib.go.opencensus.io/exporter/stackdriver"
|
name = "contrib.go.opencensus.io/exporter/stackdriver"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
||||||
[[constraint]]
|
|
||||||
branch = "master"
|
|
||||||
name = "github.com/GoogleCloudPlatform/microservices-demo"
|
|
||||||
|
|
||||||
[[constraint]]
|
[[constraint]]
|
||||||
name = "github.com/golang/protobuf"
|
name = "github.com/golang/protobuf"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
|
|
@ -80,7 +80,7 @@ func (fe *frontendServer) homeHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
"products": ps,
|
"products": ps,
|
||||||
"cart_size": len(cart),
|
"cart_size": len(cart),
|
||||||
"banner_color": os.Getenv("BANNER_COLOR"), // illustrates canary deployments
|
"banner_color": os.Getenv("BANNER_COLOR"), // illustrates canary deployments
|
||||||
"ad": fe.chooseAd(r.Context(), log),
|
"ad": fe.chooseAd(r.Context(), []string{}, log),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
log.Error(err)
|
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{}{
|
if err := templates.ExecuteTemplate(w, "product", map[string]interface{}{
|
||||||
"session_id": sessionID(r),
|
"session_id": sessionID(r),
|
||||||
"request_id": r.Context().Value(ctxKeyRequestID{}),
|
"request_id": r.Context().Value(ctxKeyRequestID{}),
|
||||||
"ad": fe.chooseAd(r.Context(), log),
|
"ad": fe.chooseAd(r.Context(), p.Categories, log),
|
||||||
"user_currency": currentCurrency(r),
|
"user_currency": currentCurrency(r),
|
||||||
"currencies": currencies,
|
"currencies": currencies,
|
||||||
"product": product,
|
"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
|
// chooseAd queries for advertisements available and randomly chooses one, if
|
||||||
// available. It ignores the error retrieving the ad since it is not critical.
|
// 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 {
|
func (fe *frontendServer) chooseAd(ctx context.Context, ctxKeys []string, log logrus.FieldLogger) *pb.Ad {
|
||||||
ads, err := fe.getAd(ctx)
|
ads, err := fe.getAd(ctx, ctxKeys)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.WithField("error", err).Warn("failed to retrieve ads")
|
log.WithField("error", err).Warn("failed to retrieve ads")
|
||||||
return nil
|
return nil
|
||||||
|
|
|
@ -116,12 +116,12 @@ func (fe *frontendServer) getRecommendations(ctx context.Context, userID string,
|
||||||
return out, err
|
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)
|
ctx, cancel := context.WithTimeout(ctx, time.Millisecond*100)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
resp, err := pb.NewAdServiceClient(fe.adSvcConn).GetAds(ctx, &pb.AdRequest{
|
resp, err := pb.NewAdServiceClient(fe.adSvcConn).GetAds(ctx, &pb.AdRequest{
|
||||||
ContextKeys: nil,
|
ContextKeys: ctxKeys,
|
||||||
})
|
})
|
||||||
return resp.GetAds(), errors.Wrap(err, "failed to get ads")
|
return resp.GetAds(), errors.Wrap(err, "failed to get ads")
|
||||||
}
|
}
|
||||||
|
|
|
@ -64,6 +64,10 @@ message Product {
|
||||||
string description = 3;
|
string description = 3;
|
||||||
string picture = 4;
|
string picture = 4;
|
||||||
Money price_usd = 5;
|
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 {
|
message ListProductsResponse {
|
||||||
|
@ -218,18 +222,18 @@ message PlaceOrderResponse {
|
||||||
OrderResult order = 1;
|
OrderResult order = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ------------Ads service------------------
|
// ------------Ad service------------------
|
||||||
|
|
||||||
service AdsService {
|
service AdService {
|
||||||
rpc GetAds(AdsRequest) returns (AdsResponse) {}
|
rpc GetAds(AdRequest) returns (AdResponse) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdsRequest {
|
message AdRequest {
|
||||||
// List of important key words from the current page describing the context.
|
// List of important key words from the current page describing the context.
|
||||||
repeated string context_keys = 1;
|
repeated string context_keys = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
message AdsResponse {
|
message AdResponse {
|
||||||
repeated Ad ads = 1;
|
repeated Ad ads = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
"currencyCode": "USD",
|
"currencyCode": "USD",
|
||||||
"units": 67,
|
"units": 67,
|
||||||
"nanos": 990000000
|
"nanos": 990000000
|
||||||
}
|
},
|
||||||
|
"categories": ["vintage"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "66VCHSJNUP",
|
"id": "66VCHSJNUP",
|
||||||
|
@ -20,7 +21,8 @@
|
||||||
"currencyCode": "USD",
|
"currencyCode": "USD",
|
||||||
"units": 12,
|
"units": 12,
|
||||||
"nanos": 490000000
|
"nanos": 490000000
|
||||||
}
|
},
|
||||||
|
"categories": ["photography", "vintage"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "1YMWWN1N4O",
|
"id": "1YMWWN1N4O",
|
||||||
|
@ -30,7 +32,8 @@
|
||||||
"priceUsd": {
|
"priceUsd": {
|
||||||
"currencyCode": "USD",
|
"currencyCode": "USD",
|
||||||
"units": 124
|
"units": 124
|
||||||
}
|
},
|
||||||
|
"categories": ["cookware"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "L9ECAV7KIM",
|
"id": "L9ECAV7KIM",
|
||||||
|
@ -41,7 +44,8 @@
|
||||||
"currencyCode": "USD",
|
"currencyCode": "USD",
|
||||||
"units": 36,
|
"units": 36,
|
||||||
"nanos": 450000000
|
"nanos": 450000000
|
||||||
}
|
},
|
||||||
|
"categories": ["gardening"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "2ZYFJ3GM2N",
|
"id": "2ZYFJ3GM2N",
|
||||||
|
@ -51,7 +55,8 @@
|
||||||
"priceUsd": {
|
"priceUsd": {
|
||||||
"currencyCode": "USD",
|
"currencyCode": "USD",
|
||||||
"units": 2245
|
"units": 2245
|
||||||
}
|
},
|
||||||
|
"categories": ["photography", "vintage"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "0PUK6V6EV0",
|
"id": "0PUK6V6EV0",
|
||||||
|
@ -62,7 +67,8 @@
|
||||||
"currencyCode": "USD",
|
"currencyCode": "USD",
|
||||||
"units": 65,
|
"units": 65,
|
||||||
"nanos": 500000000
|
"nanos": 500000000
|
||||||
}
|
},
|
||||||
|
"categories": ["music", "vintage"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "LS4PSXUNUM",
|
"id": "LS4PSXUNUM",
|
||||||
|
@ -73,7 +79,8 @@
|
||||||
"currencyCode": "USD",
|
"currencyCode": "USD",
|
||||||
"units": 24,
|
"units": 24,
|
||||||
"nanos": 330000000
|
"nanos": 330000000
|
||||||
}
|
},
|
||||||
|
"categories": ["cookware"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "9SIQT8TOJO",
|
"id": "9SIQT8TOJO",
|
||||||
|
@ -84,7 +91,8 @@
|
||||||
"currencyCode": "USD",
|
"currencyCode": "USD",
|
||||||
"units": 789,
|
"units": 789,
|
||||||
"nanos": 500000000
|
"nanos": 500000000
|
||||||
}
|
},
|
||||||
|
"categories": ["cycling"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"id": "6E92ZMYYFZ",
|
"id": "6E92ZMYYFZ",
|
||||||
|
@ -95,7 +103,8 @@
|
||||||
"currencyCode": "USD",
|
"currencyCode": "USD",
|
||||||
"units": 12,
|
"units": 12,
|
||||||
"nanos": 300000000
|
"nanos": 300000000
|
||||||
}
|
},
|
||||||
|
"categories": ["gardening"]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue