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;
|
||||
|
||||
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
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
11
src/frontend/Gopkg.lock
generated
11
src/frontend/Gopkg.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in a new issue