mirror of
https://github.com/twitter/the-algorithm.git
synced 2025-01-05 09:01:54 +01:00
Compare commits
2 Commits
90d7ea370e
...
b389c3d302
Author | SHA1 | Date | |
---|---|---|---|
|
b389c3d302 | ||
|
01dbfee4c0 |
11
README.md
11
README.md
@ -24,6 +24,7 @@ Product surfaces at Twitter are built on a shared set of data, models, and softw
|
|||||||
| | [timelines-aggregation-framework](timelines/data_processing/ml_util/aggregation_framework/README.md) | Framework for generating aggregate features in batch or real time. |
|
| | [timelines-aggregation-framework](timelines/data_processing/ml_util/aggregation_framework/README.md) | Framework for generating aggregate features in batch or real time. |
|
||||||
| | [representation-manager](representation-manager/README.md) | Service to retrieve embeddings (i.e. SimClusers and TwHIN). |
|
| | [representation-manager](representation-manager/README.md) | Service to retrieve embeddings (i.e. SimClusers and TwHIN). |
|
||||||
| | [twml](twml/README.md) | Legacy machine learning framework built on TensorFlow v1. |
|
| | [twml](twml/README.md) | Legacy machine learning framework built on TensorFlow v1. |
|
||||||
|
| | [Tweetypie](tweetypie/server/README.md) | Core Tweet service that handles the reading and writing of Tweet data. |
|
||||||
|
|
||||||
The product surface currently included in this repository is the For You Timeline.
|
The product surface currently included in this repository is the For You Timeline.
|
||||||
|
|
||||||
@ -47,6 +48,16 @@ The core components of the For You Timeline included in this repository are list
|
|||||||
| | [visibility-filters](visibilitylib/README.md) | Responsible for filtering Twitter content to support legal compliance, improve product quality, increase user trust, protect revenue through the use of hard-filtering, visible product treatments, and coarse-grained downranking. |
|
| | [visibility-filters](visibilitylib/README.md) | Responsible for filtering Twitter content to support legal compliance, improve product quality, increase user trust, protect revenue through the use of hard-filtering, visible product treatments, and coarse-grained downranking. |
|
||||||
| | [timelineranker](timelineranker/README.md) | Legacy service which provides relevance-scored tweets from the Earlybird Search Index and UTEG service. |
|
| | [timelineranker](timelineranker/README.md) | Legacy service which provides relevance-scored tweets from the Earlybird Search Index and UTEG service. |
|
||||||
|
|
||||||
|
### Recommended Notifications
|
||||||
|
|
||||||
|
The core components that power Recommended Notifications included in this repository are listed below:
|
||||||
|
|
||||||
|
| Type | Component | Description |
|
||||||
|
|------------|------------|------------|
|
||||||
|
| Service | [pushservice](pushservice/README.md) | Main recommendation service at Twitter used to surface recommendations to our users via notifications.
|
||||||
|
| Ranking | [pushservice-light-ranker](pushservice/src/main/python/models/light_ranking/README.md) | Light Ranker model used by pushservice to rank Tweets. Bridges candidate generation and heavy ranking by pre-selecting highly-relevant candidates from the initial huge candidate pool. |
|
||||||
|
| | [pushservice-heavy-ranker](pushservice/src/main/python/models/heavy_ranking/README.md) | Multi-task learning model to predict the probabilities that the target users will open and engage with the sent notifications. |
|
||||||
|
|
||||||
## Build and test code
|
## Build and test code
|
||||||
|
|
||||||
We include Bazel BUILD files for most components, but not a top-level BUILD or WORKSPACE file. We plan to add a more complete build and test system in the future.
|
We include Bazel BUILD files for most components, but not a top-level BUILD or WORKSPACE file. We plan to add a more complete build and test system in the future.
|
||||||
|
48
pushservice/BUILD.bazel
Normal file
48
pushservice/BUILD.bazel
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
alias(
|
||||||
|
name = "frigate-pushservice",
|
||||||
|
target = ":frigate-pushservice_lib",
|
||||||
|
)
|
||||||
|
|
||||||
|
target(
|
||||||
|
name = "frigate-pushservice_lib",
|
||||||
|
dependencies = [
|
||||||
|
"frigate/frigate-pushservice-opensource/src/main/scala/com/twitter/frigate/pushservice",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
jvm_binary(
|
||||||
|
name = "bin",
|
||||||
|
basename = "frigate-pushservice",
|
||||||
|
main = "com.twitter.frigate.pushservice.PushServiceMain",
|
||||||
|
runtime_platform = "java11",
|
||||||
|
tags = ["bazel-compatible"],
|
||||||
|
dependencies = [
|
||||||
|
"3rdparty/jvm/ch/qos/logback:logback-classic",
|
||||||
|
"finatra/inject/inject-logback/src/main/scala",
|
||||||
|
"frigate/frigate-pushservice-opensource/src/main/scala/com/twitter/frigate/pushservice",
|
||||||
|
"loglens/loglens-logback/src/main/scala/com/twitter/loglens/logback",
|
||||||
|
"twitter-server/logback-classic/src/main/scala",
|
||||||
|
],
|
||||||
|
excludes = [
|
||||||
|
exclude("com.twitter.translations", "translations-twitter"),
|
||||||
|
exclude("org.apache.hadoop", "hadoop-aws"),
|
||||||
|
exclude("org.tensorflow"),
|
||||||
|
scala_exclude("com.twitter", "ckoia-scala"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
jvm_app(
|
||||||
|
name = "bundle",
|
||||||
|
basename = "frigate-pushservice-package-dist",
|
||||||
|
archive = "zip",
|
||||||
|
binary = ":bin",
|
||||||
|
tags = ["bazel-compatible"],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "mr_model_constants",
|
||||||
|
sources = [
|
||||||
|
"config/deepbird/constants.py",
|
||||||
|
],
|
||||||
|
tags = ["bazel-compatible"],
|
||||||
|
)
|
45
pushservice/readme.md
Normal file
45
pushservice/readme.md
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Pushservice
|
||||||
|
|
||||||
|
Pushservice is the main push recommendation service at Twitter used to generate recommendation-based notifications for users. It currently powers two functionalities:
|
||||||
|
|
||||||
|
- RefreshForPushHandler: This handler determines whether to send a recommendation push to a user based on their ID. It generates the best push recommendation item and coordinates with downstream services to deliver it
|
||||||
|
- SendHandler: This handler determines and manage whether send the push to users based on the given target user details and the provided push recommendation item
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
### RefreshForPushHandler
|
||||||
|
|
||||||
|
RefreshForPushHandler follows these steps:
|
||||||
|
|
||||||
|
- Building Target and checking eligibility
|
||||||
|
- Builds a target user object based on the given user ID
|
||||||
|
- Performs target-level filterings to determine if the target is eligible for a recommendation push
|
||||||
|
- Fetch Candidates
|
||||||
|
- Retrieves a list of potential candidates for the push by querying various candidate sources using the target
|
||||||
|
- Candidate Hydration
|
||||||
|
- Hydrates the candidate details with batch calls to different downstream services.
|
||||||
|
- Pre-rank Filtering, also called Light Filtering
|
||||||
|
- Filters the hydrated candidates with lightweight RPC calls.
|
||||||
|
- Rank
|
||||||
|
- Perform feature hydration for candidates and target user
|
||||||
|
- Performs light ranking on candidates
|
||||||
|
- Performs heavy ranking on candidates
|
||||||
|
- Take Step, also called Heavy Filtering
|
||||||
|
- Takes the top-ranked candidates one by one and applies heavy filtering until one candidate passes all filter steps
|
||||||
|
- Send
|
||||||
|
- Calls the appropriate downstream service to deliver the eligible candidate as a push and in-app notification to the target user
|
||||||
|
|
||||||
|
### SendHandler
|
||||||
|
|
||||||
|
SendHandler follows these steps:
|
||||||
|
|
||||||
|
- Building Target
|
||||||
|
- Builds a target user object based on the given user ID
|
||||||
|
- Candidate Hydration
|
||||||
|
- Hydrates the candidate details with batch calls to different downstream services.
|
||||||
|
- Feature Hydration
|
||||||
|
- Perform feature hydration for candidates and target user
|
||||||
|
- Take Step, also called Heavy Filtering
|
||||||
|
- Perform filterings and validation checking for the given candidate
|
||||||
|
- Send
|
||||||
|
- Calls the appropriate downstream service to deliver the given candidate as a push and/or in-app notification to the target user
|
169
pushservice/src/main/python/models/heavy_ranking/BUILD
Normal file
169
pushservice/src/main/python/models/heavy_ranking/BUILD
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
python37_binary(
|
||||||
|
name = "update_warm_start_checkpoint",
|
||||||
|
source = "update_warm_start_checkpoint.py",
|
||||||
|
tags = ["no-mypy"],
|
||||||
|
dependencies = [
|
||||||
|
":deep_norm_lib",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/heavy_ranking:update_warm_start_checkpoint",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "params_lib",
|
||||||
|
sources = ["params.py"],
|
||||||
|
tags = ["no-mypy"],
|
||||||
|
dependencies = [
|
||||||
|
"3rdparty/python/pydantic:default",
|
||||||
|
"src/python/twitter/deepbird/projects/magic_recs/v11/lib:params_lib",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "features_lib",
|
||||||
|
sources = ["features.py"],
|
||||||
|
tags = ["no-mypy"],
|
||||||
|
dependencies = [
|
||||||
|
":params_lib",
|
||||||
|
"src/python/twitter/deepbird/projects/magic_recs/libs",
|
||||||
|
"twml:twml-nodeps",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "model_pools_lib",
|
||||||
|
sources = ["model_pools.py"],
|
||||||
|
tags = ["no-mypy"],
|
||||||
|
dependencies = [
|
||||||
|
":features_lib",
|
||||||
|
":params_lib",
|
||||||
|
"src/python/twitter/deepbird/projects/magic_recs/v11/lib:model_lib",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "graph_lib",
|
||||||
|
sources = ["graph.py"],
|
||||||
|
tags = ["no-mypy"],
|
||||||
|
dependencies = [
|
||||||
|
":params_lib",
|
||||||
|
"src/python/twitter/deepbird/projects/magic_recs/libs",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "run_args_lib",
|
||||||
|
sources = ["run_args.py"],
|
||||||
|
tags = ["no-mypy"],
|
||||||
|
dependencies = [
|
||||||
|
":features_lib",
|
||||||
|
":params_lib",
|
||||||
|
"twml:twml-nodeps",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "deep_norm_lib",
|
||||||
|
sources = ["deep_norm.py"],
|
||||||
|
tags = ["no-mypy"],
|
||||||
|
dependencies = [
|
||||||
|
":features_lib",
|
||||||
|
":graph_lib",
|
||||||
|
":model_pools_lib",
|
||||||
|
":params_lib",
|
||||||
|
":run_args_lib",
|
||||||
|
"src/python/twitter/deepbird/projects/magic_recs/libs",
|
||||||
|
"src/python/twitter/deepbird/util/data",
|
||||||
|
"twml:twml-nodeps",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "eval_lib",
|
||||||
|
sources = ["eval.py"],
|
||||||
|
tags = ["no-mypy"],
|
||||||
|
dependencies = [
|
||||||
|
":features_lib",
|
||||||
|
":graph_lib",
|
||||||
|
":model_pools_lib",
|
||||||
|
":params_lib",
|
||||||
|
":run_args_lib",
|
||||||
|
"src/python/twitter/deepbird/projects/magic_recs/libs",
|
||||||
|
"twml:twml-nodeps",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "deep_norm",
|
||||||
|
source = "deep_norm.py",
|
||||||
|
dependencies = [
|
||||||
|
":deep_norm_lib",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/heavy_ranking:deep_norm",
|
||||||
|
"twml",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "eval",
|
||||||
|
source = "eval.py",
|
||||||
|
dependencies = [
|
||||||
|
":eval_lib",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/heavy_ranking:eval",
|
||||||
|
"twml",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "mlwf_libs",
|
||||||
|
tags = ["no-mypy"],
|
||||||
|
dependencies = [
|
||||||
|
":deep_norm_lib",
|
||||||
|
"twml",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "train_model",
|
||||||
|
source = "deep_norm.py",
|
||||||
|
dependencies = [
|
||||||
|
":deep_norm_lib",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/heavy_ranking:train_model",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "train_model_local",
|
||||||
|
source = "deep_norm.py",
|
||||||
|
dependencies = [
|
||||||
|
":deep_norm_lib",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/heavy_ranking:train_model_local",
|
||||||
|
"twml",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "eval_model_local",
|
||||||
|
source = "eval.py",
|
||||||
|
dependencies = [
|
||||||
|
":eval_lib",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/heavy_ranking:eval_model_local",
|
||||||
|
"twml",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "eval_model",
|
||||||
|
source = "eval.py",
|
||||||
|
dependencies = [
|
||||||
|
":eval_lib",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/heavy_ranking:eval_model",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "mlwf_model",
|
||||||
|
source = "deep_norm.py",
|
||||||
|
dependencies = [
|
||||||
|
":mlwf_libs",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/heavy_ranking:mlwf_model",
|
||||||
|
],
|
||||||
|
)
|
20
pushservice/src/main/python/models/heavy_ranking/README.md
Normal file
20
pushservice/src/main/python/models/heavy_ranking/README.md
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# Notification Heavy Ranker Model
|
||||||
|
|
||||||
|
## Model Context
|
||||||
|
There are 4 major components of Twitter notifications recommendation system: 1) candidate generation 2) light ranking 3) heavy ranking & 4) quality control. This notification heavy ranker model is the core ranking model for the personalised notifications recommendation. It's a multi-task learning model to predict the probabilities that the target users will open and engage with the sent notifications.
|
||||||
|
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
- BUILD: this file defines python library dependencies
|
||||||
|
- deep_norm.py: this file contains how to set up continuous training, model evaluation and model exporting for the notification heavy ranker model
|
||||||
|
- eval.py: the main python entry file to set up the overall model evaluation pipeline
|
||||||
|
- features.py: this file contains importing feature list and support functions for feature engineering
|
||||||
|
- graph.py: this file defines how to build the tensorflow graph with specified model architecture, loss function and training configuration
|
||||||
|
- model_pools.py: this file defines the available model types for the heavy ranker
|
||||||
|
- params.py: this file defines hyper-parameters used in the notification heavy ranker
|
||||||
|
- run_args.py: this file defines command line parameters to run model training & evaluation
|
||||||
|
- update_warm_start_checkpoint.py: this file contains the support to modify checkpoints of the given saved heavy ranker model
|
||||||
|
- lib/BUILD: this file defines python library dependencies for tensorflow model architecture
|
||||||
|
- lib/layers.py: this file defines different type of convolution layers to be used in the heavy ranker model
|
||||||
|
- lib/model.py: this file defines the module containing ClemNet, the heavy ranker model type
|
||||||
|
- lib/params.py: this file defines parameters used in the heavy ranker model
|
136
pushservice/src/main/python/models/heavy_ranking/deep_norm.py
Normal file
136
pushservice/src/main/python/models/heavy_ranking/deep_norm.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
"""
|
||||||
|
Training job for the heavy ranker of the push notification service.
|
||||||
|
"""
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
|
||||||
|
import twml
|
||||||
|
|
||||||
|
from ..libs.metric_fn_utils import flip_disliked_labels, get_metric_fn
|
||||||
|
from ..libs.model_utils import read_config
|
||||||
|
from ..libs.warm_start_utils import get_feature_list_for_heavy_ranking, warm_start_checkpoint
|
||||||
|
from .features import get_feature_config
|
||||||
|
from .model_pools import ALL_MODELS
|
||||||
|
from .params import load_graph_params
|
||||||
|
from .run_args import get_training_arg_parser
|
||||||
|
|
||||||
|
import tensorflow.compat.v1 as tf
|
||||||
|
from tensorflow.compat.v1 import logging
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
args, _ = get_training_arg_parser().parse_known_args()
|
||||||
|
logging.info(f"Parsed args: {args}")
|
||||||
|
|
||||||
|
params = load_graph_params(args)
|
||||||
|
logging.info(f"Loaded graph params: {params}")
|
||||||
|
|
||||||
|
param_file = os.path.join(args.save_dir, "params.json")
|
||||||
|
logging.info(f"Saving graph params to: {param_file}")
|
||||||
|
with tf.io.gfile.GFile(param_file, mode="w") as file:
|
||||||
|
json.dump(params.json(), file, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
logging.info(f"Get Feature Config: {args.feature_list}")
|
||||||
|
feature_list = read_config(args.feature_list).items()
|
||||||
|
feature_config = get_feature_config(
|
||||||
|
data_spec_path=args.data_spec,
|
||||||
|
params=params,
|
||||||
|
feature_list_provided=feature_list,
|
||||||
|
)
|
||||||
|
feature_list_path = args.feature_list
|
||||||
|
|
||||||
|
warm_start_from = args.warm_start_from
|
||||||
|
if args.warm_start_base_dir:
|
||||||
|
logging.info(f"Get warm started model from: {args.warm_start_base_dir}.")
|
||||||
|
|
||||||
|
continuous_binary_feat_list_save_path = os.path.join(
|
||||||
|
args.warm_start_base_dir, "continuous_binary_feat_list.json"
|
||||||
|
)
|
||||||
|
warm_start_folder = os.path.join(args.warm_start_base_dir, "best_checkpoint")
|
||||||
|
job_name = os.path.basename(args.save_dir)
|
||||||
|
ws_output_ckpt_folder = os.path.join(args.warm_start_base_dir, f"warm_start_for_{job_name}")
|
||||||
|
if tf.io.gfile.exists(ws_output_ckpt_folder):
|
||||||
|
tf.io.gfile.rmtree(ws_output_ckpt_folder)
|
||||||
|
|
||||||
|
tf.io.gfile.mkdir(ws_output_ckpt_folder)
|
||||||
|
|
||||||
|
warm_start_from = warm_start_checkpoint(
|
||||||
|
warm_start_folder,
|
||||||
|
continuous_binary_feat_list_save_path,
|
||||||
|
feature_list_path,
|
||||||
|
args.data_spec,
|
||||||
|
ws_output_ckpt_folder,
|
||||||
|
)
|
||||||
|
logging.info(f"Created warm_start_from_ckpt {warm_start_from}.")
|
||||||
|
|
||||||
|
logging.info("Build Trainer.")
|
||||||
|
metric_fn = get_metric_fn("OONC_Engagement" if len(params.tasks) == 2 else "OONC", False)
|
||||||
|
|
||||||
|
trainer = twml.trainers.DataRecordTrainer(
|
||||||
|
name="magic_recs",
|
||||||
|
params=args,
|
||||||
|
build_graph_fn=lambda *args: ALL_MODELS[params.model.name](params=params)(*args),
|
||||||
|
save_dir=args.save_dir,
|
||||||
|
run_config=None,
|
||||||
|
feature_config=feature_config,
|
||||||
|
metric_fn=flip_disliked_labels(metric_fn),
|
||||||
|
warm_start_from=warm_start_from,
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.info("Build train and eval input functions.")
|
||||||
|
train_input_fn = trainer.get_train_input_fn(shuffle=True)
|
||||||
|
eval_input_fn = trainer.get_eval_input_fn(repeat=False, shuffle=False)
|
||||||
|
|
||||||
|
learn = trainer.learn
|
||||||
|
if args.distributed or args.num_workers is not None:
|
||||||
|
learn = trainer.train_and_evaluate
|
||||||
|
|
||||||
|
if not args.directly_export_best:
|
||||||
|
logging.info("Starting training")
|
||||||
|
start = datetime.now()
|
||||||
|
learn(
|
||||||
|
early_stop_minimize=False,
|
||||||
|
early_stop_metric="pr_auc_unweighted_OONC",
|
||||||
|
early_stop_patience=args.early_stop_patience,
|
||||||
|
early_stop_tolerance=args.early_stop_tolerance,
|
||||||
|
eval_input_fn=eval_input_fn,
|
||||||
|
train_input_fn=train_input_fn,
|
||||||
|
)
|
||||||
|
logging.info(f"Total training time: {datetime.now() - start}")
|
||||||
|
else:
|
||||||
|
logging.info("Directly exporting the model")
|
||||||
|
|
||||||
|
if not args.export_dir:
|
||||||
|
args.export_dir = os.path.join(args.save_dir, "exported_models")
|
||||||
|
|
||||||
|
logging.info(f"Exporting the model to {args.export_dir}.")
|
||||||
|
start = datetime.now()
|
||||||
|
twml.contrib.export.export_fn.export_all_models(
|
||||||
|
trainer=trainer,
|
||||||
|
export_dir=args.export_dir,
|
||||||
|
parse_fn=feature_config.get_parse_fn(),
|
||||||
|
serving_input_receiver_fn=feature_config.get_serving_input_receiver_fn(),
|
||||||
|
export_output_fn=twml.export_output_fns.batch_prediction_continuous_output_fn,
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.info(f"Total model export time: {datetime.now() - start}")
|
||||||
|
logging.info(f"The MLP directory is: {args.save_dir}")
|
||||||
|
|
||||||
|
continuous_binary_feat_list_save_path = os.path.join(
|
||||||
|
args.save_dir, "continuous_binary_feat_list.json"
|
||||||
|
)
|
||||||
|
logging.info(
|
||||||
|
f"Saving the list of continuous and binary features to {continuous_binary_feat_list_save_path}."
|
||||||
|
)
|
||||||
|
continuous_binary_feat_list = get_feature_list_for_heavy_ranking(
|
||||||
|
feature_list_path, args.data_spec
|
||||||
|
)
|
||||||
|
twml.util.write_file(
|
||||||
|
continuous_binary_feat_list_save_path, continuous_binary_feat_list, encode="json"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
logging.info("Done.")
|
59
pushservice/src/main/python/models/heavy_ranking/eval.py
Normal file
59
pushservice/src/main/python/models/heavy_ranking/eval.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
"""
|
||||||
|
Evaluation job for the heavy ranker of the push notification service.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import twml
|
||||||
|
|
||||||
|
from ..libs.metric_fn_utils import get_metric_fn
|
||||||
|
from ..libs.model_utils import read_config
|
||||||
|
from .features import get_feature_config
|
||||||
|
from .model_pools import ALL_MODELS
|
||||||
|
from .params import load_graph_params
|
||||||
|
from .run_args import get_eval_arg_parser
|
||||||
|
|
||||||
|
from tensorflow.compat.v1 import logging
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
args, _ = get_eval_arg_parser().parse_known_args()
|
||||||
|
logging.info(f"Parsed args: {args}")
|
||||||
|
|
||||||
|
params = load_graph_params(args)
|
||||||
|
logging.info(f"Loaded graph params: {params}")
|
||||||
|
|
||||||
|
logging.info(f"Get Feature Config: {args.feature_list}")
|
||||||
|
feature_list = read_config(args.feature_list).items()
|
||||||
|
feature_config = get_feature_config(
|
||||||
|
data_spec_path=args.data_spec,
|
||||||
|
params=params,
|
||||||
|
feature_list_provided=feature_list,
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.info("Build DataRecordTrainer.")
|
||||||
|
metric_fn = get_metric_fn("OONC_Engagement" if len(params.tasks) == 2 else "OONC", False)
|
||||||
|
|
||||||
|
trainer = twml.trainers.DataRecordTrainer(
|
||||||
|
name="magic_recs",
|
||||||
|
params=args,
|
||||||
|
build_graph_fn=lambda *args: ALL_MODELS[params.model.name](params=params)(*args),
|
||||||
|
save_dir=args.save_dir,
|
||||||
|
run_config=None,
|
||||||
|
feature_config=feature_config,
|
||||||
|
metric_fn=metric_fn,
|
||||||
|
)
|
||||||
|
|
||||||
|
logging.info("Run the evaluation.")
|
||||||
|
start = datetime.now()
|
||||||
|
trainer._estimator.evaluate(
|
||||||
|
input_fn=trainer.get_eval_input_fn(repeat=False, shuffle=False),
|
||||||
|
steps=None if (args.eval_steps is not None and args.eval_steps < 0) else args.eval_steps,
|
||||||
|
checkpoint_path=args.eval_checkpoint,
|
||||||
|
)
|
||||||
|
logging.info(f"Evaluating time: {datetime.now() - start}.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
|
logging.info("Job done.")
|
138
pushservice/src/main/python/models/heavy_ranking/features.py
Normal file
138
pushservice/src/main/python/models/heavy_ranking/features.py
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
import os
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from twitter.deepbird.projects.magic_recs.libs.model_utils import filter_nans_and_infs
|
||||||
|
import twml
|
||||||
|
from twml.layers import full_sparse, sparse_max_norm
|
||||||
|
|
||||||
|
from .params import FeaturesParams, GraphParams, SparseFeaturesParams
|
||||||
|
|
||||||
|
import tensorflow as tf
|
||||||
|
from tensorflow import Tensor
|
||||||
|
import tensorflow.compat.v1 as tf1
|
||||||
|
|
||||||
|
|
||||||
|
FEAT_CONFIG_DEFAULT_VAL = 0
|
||||||
|
DEFAULT_FEATURE_LIST_PATH = "./feature_list_default.yaml"
|
||||||
|
FEATURE_LIST_DEFAULT_PATH = os.path.join(
|
||||||
|
os.path.dirname(os.path.realpath(__file__)), DEFAULT_FEATURE_LIST_PATH
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_feature_config(data_spec_path=None, feature_list_provided=[], params: GraphParams = None):
|
||||||
|
|
||||||
|
a_string_feat_list = [feat for feat, feat_type in feature_list_provided if feat_type != "S"]
|
||||||
|
|
||||||
|
builder = twml.contrib.feature_config.FeatureConfigBuilder(
|
||||||
|
data_spec_path=data_spec_path, debug=False
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = builder.extract_feature_group(
|
||||||
|
feature_regexes=a_string_feat_list,
|
||||||
|
group_name="continuous_features",
|
||||||
|
default_value=FEAT_CONFIG_DEFAULT_VAL,
|
||||||
|
type_filter=["CONTINUOUS"],
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = builder.extract_feature_group(
|
||||||
|
feature_regexes=a_string_feat_list,
|
||||||
|
group_name="binary_features",
|
||||||
|
type_filter=["BINARY"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if params.model.features.sparse_features:
|
||||||
|
builder = builder.extract_features_as_hashed_sparse(
|
||||||
|
feature_regexes=a_string_feat_list,
|
||||||
|
hash_space_size_bits=params.model.features.sparse_features.bits,
|
||||||
|
type_filter=["DISCRETE", "STRING", "SPARSE_BINARY"],
|
||||||
|
output_tensor_name="sparse_not_continuous",
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = builder.extract_features_as_hashed_sparse(
|
||||||
|
feature_regexes=[feat for feat, feat_type in feature_list_provided if feat_type == "S"],
|
||||||
|
hash_space_size_bits=params.model.features.sparse_features.bits,
|
||||||
|
type_filter=["SPARSE_CONTINUOUS"],
|
||||||
|
output_tensor_name="sparse_continuous",
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = builder.add_labels([task.label for task in params.tasks] + ["label.ntabDislike"])
|
||||||
|
|
||||||
|
if params.weight:
|
||||||
|
builder = builder.define_weight(params.weight)
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
|
||||||
|
|
||||||
|
def dense_features(features: Dict[str, Tensor], training: bool) -> Tensor:
|
||||||
|
"""
|
||||||
|
Performs feature transformations on the raw dense features (continuous and binary).
|
||||||
|
"""
|
||||||
|
with tf.name_scope("dense_features"):
|
||||||
|
x = filter_nans_and_infs(features["continuous_features"])
|
||||||
|
|
||||||
|
x = tf.sign(x) * tf.math.log(tf.abs(x) + 1)
|
||||||
|
x = tf1.layers.batch_normalization(
|
||||||
|
x, momentum=0.9999, training=training, renorm=training, axis=1
|
||||||
|
)
|
||||||
|
x = tf.clip_by_value(x, -5, 5)
|
||||||
|
|
||||||
|
transformed_continous_features = tf.where(tf.math.is_nan(x), tf.zeros_like(x), x)
|
||||||
|
|
||||||
|
binary_features = filter_nans_and_infs(features["binary_features"])
|
||||||
|
binary_features = tf.dtypes.cast(binary_features, tf.float32)
|
||||||
|
|
||||||
|
output = tf.concat([transformed_continous_features, binary_features], axis=1)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def sparse_features(
|
||||||
|
features: Dict[str, Tensor], training: bool, params: SparseFeaturesParams
|
||||||
|
) -> Tensor:
|
||||||
|
"""
|
||||||
|
Performs feature transformations on the raw sparse features.
|
||||||
|
"""
|
||||||
|
|
||||||
|
with tf.name_scope("sparse_features"):
|
||||||
|
with tf.name_scope("sparse_not_continuous"):
|
||||||
|
sparse_not_continuous = full_sparse(
|
||||||
|
inputs=features["sparse_not_continuous"],
|
||||||
|
output_size=params.embedding_size,
|
||||||
|
use_sparse_grads=training,
|
||||||
|
use_binary_values=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
with tf.name_scope("sparse_continuous"):
|
||||||
|
shape_enforced_input = twml.util.limit_sparse_tensor_size(
|
||||||
|
sparse_tf=features["sparse_continuous"], input_size_bits=params.bits, mask_indices=False
|
||||||
|
)
|
||||||
|
|
||||||
|
normalized_continuous_sparse = sparse_max_norm(
|
||||||
|
inputs=shape_enforced_input, is_training=training
|
||||||
|
)
|
||||||
|
|
||||||
|
sparse_continuous = full_sparse(
|
||||||
|
inputs=normalized_continuous_sparse,
|
||||||
|
output_size=params.embedding_size,
|
||||||
|
use_sparse_grads=training,
|
||||||
|
use_binary_values=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
output = tf.concat([sparse_not_continuous, sparse_continuous], axis=1)
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
def get_features(features: Dict[str, Tensor], training: bool, params: FeaturesParams) -> Tensor:
|
||||||
|
"""
|
||||||
|
Performs feature transformations on the dense and sparse features and combine the resulting
|
||||||
|
tensors into a single one.
|
||||||
|
"""
|
||||||
|
with tf.name_scope("features"):
|
||||||
|
x = dense_features(features, training)
|
||||||
|
tf1.logging.info(f"Dense features: {x.shape}")
|
||||||
|
|
||||||
|
if params.sparse_features:
|
||||||
|
x = tf.concat([x, sparse_features(features, training, params.sparse_features)], axis=1)
|
||||||
|
|
||||||
|
return x
|
129
pushservice/src/main/python/models/heavy_ranking/graph.py
Normal file
129
pushservice/src/main/python/models/heavy_ranking/graph.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
"""
|
||||||
|
Graph class defining methods to obtain key quantities such as:
|
||||||
|
* the logits
|
||||||
|
* the probabilities
|
||||||
|
* the final score
|
||||||
|
* the loss function
|
||||||
|
* the training operator
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from twitter.deepbird.hparam import HParams
|
||||||
|
import twml
|
||||||
|
|
||||||
|
from ..libs.model_utils import generate_disliked_mask
|
||||||
|
from .params import GraphParams
|
||||||
|
|
||||||
|
import tensorflow as tf
|
||||||
|
import tensorflow.compat.v1 as tf1
|
||||||
|
|
||||||
|
|
||||||
|
class Graph(ABC):
|
||||||
|
def __init__(self, params: GraphParams):
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def get_logits(self, features: Dict[str, tf.Tensor], mode: tf.estimator.ModeKeys) -> tf.Tensor:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def get_probabilities(self, logits: tf.Tensor) -> tf.Tensor:
|
||||||
|
return tf.math.cumprod(tf.nn.sigmoid(logits), axis=1, name="probabilities")
|
||||||
|
|
||||||
|
def get_task_weights(self, labels: tf.Tensor) -> tf.Tensor:
|
||||||
|
oonc_label = tf.reshape(labels[:, 0], shape=(-1, 1))
|
||||||
|
task_weights = tf.concat([tf.ones_like(oonc_label), oonc_label], axis=1)
|
||||||
|
|
||||||
|
n_labels = len(self.params.tasks)
|
||||||
|
task_weights = tf.reshape(task_weights[:, 0:n_labels], shape=(-1, n_labels))
|
||||||
|
|
||||||
|
return task_weights
|
||||||
|
|
||||||
|
def get_loss(self, labels: tf.Tensor, logits: tf.Tensor, **kwargs: Any) -> tf.Tensor:
|
||||||
|
with tf.name_scope("weights"):
|
||||||
|
disliked_mask = generate_disliked_mask(labels)
|
||||||
|
|
||||||
|
labels = tf.reshape(labels[:, 0:2], shape=[-1, 2])
|
||||||
|
|
||||||
|
labels = labels * tf.cast(tf.logical_not(disliked_mask), dtype=labels.dtype)
|
||||||
|
|
||||||
|
with tf.name_scope("task_weight"):
|
||||||
|
task_weights = self.get_task_weights(labels)
|
||||||
|
|
||||||
|
with tf.name_scope("batch_size"):
|
||||||
|
batch_size = tf.cast(tf.shape(labels)[0], dtype=tf.float32, name="batch_size")
|
||||||
|
|
||||||
|
weights = task_weights / batch_size
|
||||||
|
|
||||||
|
with tf.name_scope("loss"):
|
||||||
|
loss = tf.reduce_sum(
|
||||||
|
tf.nn.sigmoid_cross_entropy_with_logits(labels=labels, logits=logits) * weights,
|
||||||
|
)
|
||||||
|
|
||||||
|
return loss
|
||||||
|
|
||||||
|
def get_score(self, probabilities: tf.Tensor) -> tf.Tensor:
|
||||||
|
with tf.name_scope("score_weight"):
|
||||||
|
score_weights = tf.constant([task.score_weight for task in self.params.tasks])
|
||||||
|
score_weights = score_weights / tf.reduce_sum(score_weights, axis=0)
|
||||||
|
|
||||||
|
with tf.name_scope("score"):
|
||||||
|
score = tf.reshape(tf.reduce_sum(probabilities * score_weights, axis=1), shape=[-1, 1])
|
||||||
|
|
||||||
|
return score
|
||||||
|
|
||||||
|
def get_train_op(self, loss: tf.Tensor, twml_params) -> Any:
|
||||||
|
with tf.name_scope("optimizer"):
|
||||||
|
learning_rate = twml_params.learning_rate
|
||||||
|
optimizer = tf1.train.GradientDescentOptimizer(learning_rate=learning_rate)
|
||||||
|
|
||||||
|
update_ops = set(tf1.get_collection(tf1.GraphKeys.UPDATE_OPS))
|
||||||
|
with tf.control_dependencies(update_ops):
|
||||||
|
train_op = twml.optimizers.optimize_loss(
|
||||||
|
loss=loss,
|
||||||
|
variables=tf1.trainable_variables(),
|
||||||
|
global_step=tf1.train.get_global_step(),
|
||||||
|
optimizer=optimizer,
|
||||||
|
learning_rate=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
return train_op
|
||||||
|
|
||||||
|
def __call__(
|
||||||
|
self,
|
||||||
|
features: Dict[str, tf.Tensor],
|
||||||
|
labels: tf.Tensor,
|
||||||
|
mode: tf.estimator.ModeKeys,
|
||||||
|
params: HParams,
|
||||||
|
config=None,
|
||||||
|
) -> Dict[str, tf.Tensor]:
|
||||||
|
training = mode == tf.estimator.ModeKeys.TRAIN
|
||||||
|
logits = self.get_logits(features=features, training=training)
|
||||||
|
probabilities = self.get_probabilities(logits=logits)
|
||||||
|
score = None
|
||||||
|
loss = None
|
||||||
|
train_op = None
|
||||||
|
|
||||||
|
if mode == tf.estimator.ModeKeys.PREDICT:
|
||||||
|
score = self.get_score(probabilities=probabilities)
|
||||||
|
output = {"loss": loss, "train_op": train_op, "prediction": score}
|
||||||
|
|
||||||
|
elif mode in (tf.estimator.ModeKeys.TRAIN, tf.estimator.ModeKeys.EVAL):
|
||||||
|
loss = self.get_loss(labels=labels, logits=logits)
|
||||||
|
|
||||||
|
if mode == tf.estimator.ModeKeys.TRAIN:
|
||||||
|
train_op = self.get_train_op(loss=loss, twml_params=params)
|
||||||
|
|
||||||
|
output = {"loss": loss, "train_op": train_op, "output": probabilities}
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"""
|
||||||
|
Invalid mode. Possible values are: {tf.estimator.ModeKeys.PREDICT}, {tf.estimator.ModeKeys.TRAIN}, and {tf.estimator.ModeKeys.EVAL}
|
||||||
|
. Passed: {mode}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
|
return output
|
42
pushservice/src/main/python/models/heavy_ranking/lib/BUILD
Normal file
42
pushservice/src/main/python/models/heavy_ranking/lib/BUILD
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
python3_library(
|
||||||
|
name = "params_lib",
|
||||||
|
sources = [
|
||||||
|
"params.py",
|
||||||
|
],
|
||||||
|
tags = [
|
||||||
|
"bazel-compatible",
|
||||||
|
"no-mypy",
|
||||||
|
],
|
||||||
|
dependencies = [
|
||||||
|
"3rdparty/python/pydantic:default",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "layers_lib",
|
||||||
|
sources = [
|
||||||
|
"layers.py",
|
||||||
|
],
|
||||||
|
tags = [
|
||||||
|
"bazel-compatible",
|
||||||
|
"no-mypy",
|
||||||
|
],
|
||||||
|
dependencies = [
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "model_lib",
|
||||||
|
sources = [
|
||||||
|
"model.py",
|
||||||
|
],
|
||||||
|
tags = [
|
||||||
|
"bazel-compatible",
|
||||||
|
"no-mypy",
|
||||||
|
],
|
||||||
|
dependencies = [
|
||||||
|
":layers_lib",
|
||||||
|
":params_lib",
|
||||||
|
"3rdparty/python/absl-py:default",
|
||||||
|
],
|
||||||
|
)
|
128
pushservice/src/main/python/models/heavy_ranking/lib/layers.py
Normal file
128
pushservice/src/main/python/models/heavy_ranking/lib/layers.py
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
"""
|
||||||
|
Different type of convolution layers to be used in the ClemNet.
|
||||||
|
"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
|
||||||
|
class KerasConv1D(tf.keras.layers.Layer):
|
||||||
|
"""
|
||||||
|
Basic Conv1D layer in a wrapper to be compatible with ClemNet.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
kernel_size: int,
|
||||||
|
filters: int,
|
||||||
|
strides: int,
|
||||||
|
padding: str,
|
||||||
|
use_bias: bool = True,
|
||||||
|
kernel_initializer: str = "glorot_uniform",
|
||||||
|
bias_initializer: str = "zeros",
|
||||||
|
**kwargs: Any,
|
||||||
|
):
|
||||||
|
super(KerasConv1D, self).__init__(**kwargs)
|
||||||
|
self.kernel_size = kernel_size
|
||||||
|
self.filters = filters
|
||||||
|
self.use_bias = use_bias
|
||||||
|
self.kernel_initializer = kernel_initializer
|
||||||
|
self.bias_initializer = bias_initializer
|
||||||
|
self.strides = strides
|
||||||
|
self.padding = padding
|
||||||
|
|
||||||
|
def build(self, input_shape: tf.TensorShape) -> None:
|
||||||
|
assert (
|
||||||
|
len(input_shape) == 3
|
||||||
|
), f"Tensor shape must be of length 3. Passed tensor of shape {input_shape}."
|
||||||
|
|
||||||
|
self.features = input_shape[1]
|
||||||
|
|
||||||
|
self.w = tf.keras.layers.Conv1D(
|
||||||
|
kernel_size=self.kernel_size,
|
||||||
|
filters=self.filters,
|
||||||
|
strides=self.strides,
|
||||||
|
padding=self.padding,
|
||||||
|
use_bias=self.use_bias,
|
||||||
|
kernel_initializer=self.kernel_initializer,
|
||||||
|
bias_initializer=self.bias_initializer,
|
||||||
|
name=self.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
def call(self, inputs: tf.Tensor, **kwargs: Any) -> tf.Tensor:
|
||||||
|
return self.w(inputs)
|
||||||
|
|
||||||
|
|
||||||
|
class ChannelWiseDense(tf.keras.layers.Layer):
|
||||||
|
"""
|
||||||
|
Dense layer is applied to each channel separately. This is more memory and computationally
|
||||||
|
efficient than flattening the channels and performing single dense layers over it which is the
|
||||||
|
default behavior in tf1.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
output_size: int,
|
||||||
|
use_bias: bool,
|
||||||
|
kernel_initializer: str = "uniform_glorot",
|
||||||
|
bias_initializer: str = "zeros",
|
||||||
|
**kwargs: Any,
|
||||||
|
):
|
||||||
|
super(ChannelWiseDense, self).__init__(**kwargs)
|
||||||
|
self.output_size = output_size
|
||||||
|
self.use_bias = use_bias
|
||||||
|
self.kernel_initializer = kernel_initializer
|
||||||
|
self.bias_initializer = bias_initializer
|
||||||
|
|
||||||
|
def build(self, input_shape: tf.TensorShape) -> None:
|
||||||
|
assert (
|
||||||
|
len(input_shape) == 3
|
||||||
|
), f"Tensor shape must be of length 3. Passed tensor of shape {input_shape}."
|
||||||
|
|
||||||
|
input_size = input_shape[1]
|
||||||
|
channels = input_shape[2]
|
||||||
|
|
||||||
|
self.kernel = self.add_weight(
|
||||||
|
name="kernel",
|
||||||
|
shape=(channels, input_size, self.output_size),
|
||||||
|
initializer=self.kernel_initializer,
|
||||||
|
trainable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.bias = self.add_weight(
|
||||||
|
name="bias",
|
||||||
|
shape=(channels, self.output_size),
|
||||||
|
initializer=self.bias_initializer,
|
||||||
|
trainable=self.use_bias,
|
||||||
|
)
|
||||||
|
|
||||||
|
def call(self, inputs: tf.Tensor, **kwargs: Any) -> tf.Tensor:
|
||||||
|
x = inputs
|
||||||
|
|
||||||
|
transposed_x = tf.transpose(x, perm=[2, 0, 1])
|
||||||
|
transposed_residual = (
|
||||||
|
tf.transpose(tf.matmul(transposed_x, self.kernel), perm=[1, 0, 2]) + self.bias
|
||||||
|
)
|
||||||
|
output = tf.transpose(transposed_residual, perm=[0, 2, 1])
|
||||||
|
|
||||||
|
return output
|
||||||
|
|
||||||
|
|
||||||
|
class ResidualLayer(tf.keras.layers.Layer):
|
||||||
|
"""
|
||||||
|
Layer implementing a 3D-residual connection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def build(self, input_shape: tf.TensorShape) -> None:
|
||||||
|
assert (
|
||||||
|
len(input_shape) == 3
|
||||||
|
), f"Tensor shape must be of length 3. Passed tensor of shape {input_shape}."
|
||||||
|
|
||||||
|
def call(self, inputs: tf.Tensor, residual: tf.Tensor, **kwargs: Any) -> tf.Tensor:
|
||||||
|
shortcut = tf.keras.layers.Conv1D(
|
||||||
|
filters=int(residual.shape[2]), strides=1, kernel_size=1, padding="SAME", use_bias=False
|
||||||
|
)(inputs)
|
||||||
|
|
||||||
|
output = tf.add(shortcut, residual)
|
||||||
|
|
||||||
|
return output
|
@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
Module containing ClemNet.
|
||||||
|
"""
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from .layers import ChannelWiseDense, KerasConv1D, ResidualLayer
|
||||||
|
from .params import BlockParams, ClemNetParams
|
||||||
|
|
||||||
|
import tensorflow as tf
|
||||||
|
import tensorflow.compat.v1 as tf1
|
||||||
|
|
||||||
|
|
||||||
|
class Block2(tf.keras.layers.Layer):
|
||||||
|
"""
|
||||||
|
Possible ClemNet block. Architecture is as follow:
|
||||||
|
Optional(DenseLayer + BN + Act)
|
||||||
|
Optional(ConvLayer + BN + Act)
|
||||||
|
Optional(Residual Layer)
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, params: BlockParams, **kwargs: Any):
|
||||||
|
super(Block2, self).__init__(**kwargs)
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
def build(self, input_shape: tf.TensorShape) -> None:
|
||||||
|
assert (
|
||||||
|
len(input_shape) == 3
|
||||||
|
), f"Tensor shape must be of length 3. Passed tensor of shape {input_shape}."
|
||||||
|
|
||||||
|
def call(self, inputs: tf.Tensor, training: bool) -> tf.Tensor:
|
||||||
|
x = inputs
|
||||||
|
if self.params.dense:
|
||||||
|
x = ChannelWiseDense(**self.params.dense.dict())(inputs=x, training=training)
|
||||||
|
x = tf1.layers.batch_normalization(x, momentum=0.9999, training=training, axis=1)
|
||||||
|
x = tf.keras.layers.Activation(self.params.activation)(x)
|
||||||
|
|
||||||
|
if self.params.conv:
|
||||||
|
x = KerasConv1D(**self.params.conv.dict())(inputs=x, training=training)
|
||||||
|
x = tf1.layers.batch_normalization(x, momentum=0.9999, training=training, axis=1)
|
||||||
|
x = tf.keras.layers.Activation(self.params.activation)(x)
|
||||||
|
|
||||||
|
if self.params.residual:
|
||||||
|
x = ResidualLayer()(inputs=inputs, residual=x)
|
||||||
|
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
class ClemNet(tf.keras.layers.Layer):
|
||||||
|
"""
|
||||||
|
A residual network stacking residual blocks composed of dense layers and convolutions.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, params: ClemNetParams, **kwargs: Any):
|
||||||
|
super(ClemNet, self).__init__(**kwargs)
|
||||||
|
self.params = params
|
||||||
|
|
||||||
|
def build(self, input_shape: tf.TensorShape) -> None:
|
||||||
|
assert len(input_shape) in (
|
||||||
|
2,
|
||||||
|
3,
|
||||||
|
), f"Tensor shape must be of length 3. Passed tensor of shape {input_shape}."
|
||||||
|
|
||||||
|
def call(self, inputs: tf.Tensor, training: bool) -> tf.Tensor:
|
||||||
|
if len(inputs.shape) < 3:
|
||||||
|
inputs = tf.expand_dims(inputs, axis=-1)
|
||||||
|
|
||||||
|
x = inputs
|
||||||
|
for block_params in self.params.blocks:
|
||||||
|
x = Block2(block_params)(inputs=x, training=training)
|
||||||
|
|
||||||
|
x = tf.keras.layers.Flatten(name="flattened")(x)
|
||||||
|
if self.params.top:
|
||||||
|
x = tf.keras.layers.Dense(units=self.params.top.n_labels, name="logits")(x)
|
||||||
|
|
||||||
|
return x
|
@ -0,0 +1,49 @@
|
|||||||
|
"""
|
||||||
|
Parameters used in ClemNet.
|
||||||
|
"""
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Extra, Field, PositiveInt
|
||||||
|
|
||||||
|
|
||||||
|
# checkstyle: noqa
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendedBaseModel(BaseModel):
|
||||||
|
class Config:
|
||||||
|
extra = Extra.forbid
|
||||||
|
|
||||||
|
|
||||||
|
class DenseParams(ExtendedBaseModel):
|
||||||
|
name: Optional[str]
|
||||||
|
bias_initializer: str = "zeros"
|
||||||
|
kernel_initializer: str = "glorot_uniform"
|
||||||
|
output_size: PositiveInt
|
||||||
|
use_bias: bool = Field(True)
|
||||||
|
|
||||||
|
|
||||||
|
class ConvParams(ExtendedBaseModel):
|
||||||
|
name: Optional[str]
|
||||||
|
bias_initializer: str = "zeros"
|
||||||
|
filters: PositiveInt
|
||||||
|
kernel_initializer: str = "glorot_uniform"
|
||||||
|
kernel_size: PositiveInt
|
||||||
|
padding: str = "SAME"
|
||||||
|
strides: PositiveInt = 1
|
||||||
|
use_bias: bool = Field(True)
|
||||||
|
|
||||||
|
|
||||||
|
class BlockParams(ExtendedBaseModel):
|
||||||
|
activation: Optional[str]
|
||||||
|
conv: Optional[ConvParams]
|
||||||
|
dense: Optional[DenseParams]
|
||||||
|
residual: Optional[bool]
|
||||||
|
|
||||||
|
|
||||||
|
class TopLayerParams(ExtendedBaseModel):
|
||||||
|
n_labels: PositiveInt
|
||||||
|
|
||||||
|
|
||||||
|
class ClemNetParams(ExtendedBaseModel):
|
||||||
|
blocks: List[BlockParams] = []
|
||||||
|
top: Optional[TopLayerParams]
|
@ -0,0 +1,34 @@
|
|||||||
|
"""
|
||||||
|
Candidate architectures for each task's.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from .features import get_features
|
||||||
|
from .graph import Graph
|
||||||
|
from .lib.model import ClemNet
|
||||||
|
from .params import ModelTypeEnum
|
||||||
|
|
||||||
|
import tensorflow as tf
|
||||||
|
|
||||||
|
|
||||||
|
class MagicRecsClemNet(Graph):
|
||||||
|
def get_logits(self, features: Dict[str, tf.Tensor], training: bool) -> tf.Tensor:
|
||||||
|
|
||||||
|
with tf.name_scope("logits"):
|
||||||
|
inputs = get_features(features=features, training=training, params=self.params.model.features)
|
||||||
|
|
||||||
|
with tf.name_scope("OONC_logits"):
|
||||||
|
model = ClemNet(params=self.params.model.architecture)
|
||||||
|
oonc_logit = model(inputs=inputs, training=training)
|
||||||
|
|
||||||
|
with tf.name_scope("EngagementGivenOONC_logits"):
|
||||||
|
model = ClemNet(params=self.params.model.architecture)
|
||||||
|
eng_logits = model(inputs=inputs, training=training)
|
||||||
|
|
||||||
|
return tf.concat([oonc_logit, eng_logits], axis=1)
|
||||||
|
|
||||||
|
|
||||||
|
ALL_MODELS = {ModelTypeEnum.clemnet: MagicRecsClemNet}
|
89
pushservice/src/main/python/models/heavy_ranking/params.py
Normal file
89
pushservice/src/main/python/models/heavy_ranking/params.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
import enum
|
||||||
|
import json
|
||||||
|
from typing import List, Optional
|
||||||
|
|
||||||
|
from .lib.params import BlockParams, ClemNetParams, ConvParams, DenseParams, TopLayerParams
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Extra, NonNegativeFloat
|
||||||
|
import tensorflow.compat.v1 as tf
|
||||||
|
|
||||||
|
|
||||||
|
# checkstyle: noqa
|
||||||
|
|
||||||
|
|
||||||
|
class ExtendedBaseModel(BaseModel):
|
||||||
|
class Config:
|
||||||
|
extra = Extra.forbid
|
||||||
|
|
||||||
|
|
||||||
|
class SparseFeaturesParams(ExtendedBaseModel):
|
||||||
|
bits: int
|
||||||
|
embedding_size: int
|
||||||
|
|
||||||
|
|
||||||
|
class FeaturesParams(ExtendedBaseModel):
|
||||||
|
sparse_features: Optional[SparseFeaturesParams]
|
||||||
|
|
||||||
|
|
||||||
|
class ModelTypeEnum(str, enum.Enum):
|
||||||
|
clemnet: str = "clemnet"
|
||||||
|
|
||||||
|
|
||||||
|
class ModelParams(ExtendedBaseModel):
|
||||||
|
name: ModelTypeEnum
|
||||||
|
features: FeaturesParams
|
||||||
|
architecture: ClemNetParams
|
||||||
|
|
||||||
|
|
||||||
|
class TaskNameEnum(str, enum.Enum):
|
||||||
|
oonc: str = "OONC"
|
||||||
|
engagement: str = "Engagement"
|
||||||
|
|
||||||
|
|
||||||
|
class Task(ExtendedBaseModel):
|
||||||
|
name: TaskNameEnum
|
||||||
|
label: str
|
||||||
|
score_weight: NonNegativeFloat
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_TASKS = [
|
||||||
|
Task(name=TaskNameEnum.oonc, label="label", score_weight=0.9),
|
||||||
|
Task(name=TaskNameEnum.engagement, label="label.engagement", score_weight=0.1),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class GraphParams(ExtendedBaseModel):
|
||||||
|
tasks: List[Task] = DEFAULT_TASKS
|
||||||
|
model: ModelParams
|
||||||
|
weight: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
DEFAULT_ARCHITECTURE_PARAMS = ClemNetParams(
|
||||||
|
blocks=[
|
||||||
|
BlockParams(
|
||||||
|
activation="relu",
|
||||||
|
conv=ConvParams(kernel_size=3, filters=5),
|
||||||
|
dense=DenseParams(output_size=output_size),
|
||||||
|
residual=False,
|
||||||
|
)
|
||||||
|
for output_size in [1024, 512, 256, 128]
|
||||||
|
],
|
||||||
|
top=TopLayerParams(n_labels=1),
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_GRAPH_PARAMS = GraphParams(
|
||||||
|
model=ModelParams(
|
||||||
|
name=ModelTypeEnum.clemnet,
|
||||||
|
architecture=DEFAULT_ARCHITECTURE_PARAMS,
|
||||||
|
features=FeaturesParams(sparse_features=SparseFeaturesParams(bits=18, embedding_size=50)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def load_graph_params(args) -> GraphParams:
|
||||||
|
params = DEFAULT_GRAPH_PARAMS
|
||||||
|
if args.param_file:
|
||||||
|
with tf.io.gfile.GFile(args.param_file, mode="r+") as file:
|
||||||
|
params = GraphParams.parse_obj(json.load(file))
|
||||||
|
|
||||||
|
return params
|
59
pushservice/src/main/python/models/heavy_ranking/run_args.py
Normal file
59
pushservice/src/main/python/models/heavy_ranking/run_args.py
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from twml.trainers import DataRecordTrainer
|
||||||
|
|
||||||
|
from .features import FEATURE_LIST_DEFAULT_PATH
|
||||||
|
|
||||||
|
|
||||||
|
def get_training_arg_parser():
|
||||||
|
parser = DataRecordTrainer.add_parser_arguments()
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--feature_list",
|
||||||
|
default=FEATURE_LIST_DEFAULT_PATH,
|
||||||
|
type=str,
|
||||||
|
help="Which features to use for training",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--param_file",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="Path to JSON file containing the graph parameters. If None, model will load default parameters.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--directly_export_best",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="whether to directly_export best_checkpoint",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--warm_start_from", default=None, type=str, help="model dir to warm start from"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--warm_start_base_dir",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="latest ckpt in this folder will be used to ",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--model_type",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="Which type of model to train.",
|
||||||
|
)
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def get_eval_arg_parser():
|
||||||
|
parser = get_training_arg_parser()
|
||||||
|
parser.add_argument(
|
||||||
|
"--eval_checkpoint",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="Which checkpoint to use for evaluation",
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser
|
@ -0,0 +1,146 @@
|
|||||||
|
"""
|
||||||
|
Model for modifying the checkpoints of the magic recs cnn Model with addition, deletion, and reordering
|
||||||
|
of continuous and binary features.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from twitter.deepbird.projects.magic_recs.libs.get_feat_config import FEATURE_LIST_DEFAULT_PATH
|
||||||
|
from twitter.deepbird.projects.magic_recs.libs.warm_start_utils_v11 import (
|
||||||
|
get_feature_list_for_heavy_ranking,
|
||||||
|
mkdirp,
|
||||||
|
rename_dir,
|
||||||
|
rmdir,
|
||||||
|
warm_start_checkpoint,
|
||||||
|
)
|
||||||
|
import twml
|
||||||
|
from twml.trainers import DataRecordTrainer
|
||||||
|
|
||||||
|
import tensorflow.compat.v1 as tf
|
||||||
|
from tensorflow.compat.v1 import logging
|
||||||
|
|
||||||
|
|
||||||
|
def get_arg_parser():
|
||||||
|
parser = DataRecordTrainer.add_parser_arguments()
|
||||||
|
parser.add_argument(
|
||||||
|
"--model_type",
|
||||||
|
default="deepnorm_gbdt_inputdrop2_rescale",
|
||||||
|
type=str,
|
||||||
|
help="specify the model type to use.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--model_trainer_name",
|
||||||
|
default="None",
|
||||||
|
type=str,
|
||||||
|
help="deprecated, added here just for api compatibility.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--warm_start_base_dir",
|
||||||
|
default="none",
|
||||||
|
type=str,
|
||||||
|
help="latest ckpt in this folder will be used.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--output_checkpoint_dir",
|
||||||
|
default="none",
|
||||||
|
type=str,
|
||||||
|
help="Output folder for warm started ckpt. If none, it will move warm_start_base_dir to backup, and overwrite it",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--feature_list",
|
||||||
|
default="none",
|
||||||
|
type=str,
|
||||||
|
help="Which features to use for training",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--old_feature_list",
|
||||||
|
default="none",
|
||||||
|
type=str,
|
||||||
|
help="Which features to use for training",
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def get_params(args=None):
|
||||||
|
parser = get_arg_parser()
|
||||||
|
if args is None:
|
||||||
|
return parser.parse_args()
|
||||||
|
else:
|
||||||
|
return parser.parse_args(args)
|
||||||
|
|
||||||
|
|
||||||
|
def _main():
|
||||||
|
opt = get_params()
|
||||||
|
logging.info("parse is: ")
|
||||||
|
logging.info(opt)
|
||||||
|
|
||||||
|
if opt.feature_list == "none":
|
||||||
|
feature_list_path = FEATURE_LIST_DEFAULT_PATH
|
||||||
|
else:
|
||||||
|
feature_list_path = opt.feature_list
|
||||||
|
|
||||||
|
if opt.warm_start_base_dir != "none" and tf.io.gfile.exists(opt.warm_start_base_dir):
|
||||||
|
if opt.output_checkpoint_dir == "none" or opt.output_checkpoint_dir == opt.warm_start_base_dir:
|
||||||
|
_warm_start_base_dir = os.path.normpath(opt.warm_start_base_dir) + "_backup_warm_start"
|
||||||
|
_output_folder_dir = opt.warm_start_base_dir
|
||||||
|
|
||||||
|
rename_dir(opt.warm_start_base_dir, _warm_start_base_dir)
|
||||||
|
tf.logging.info(f"moved {opt.warm_start_base_dir} to {_warm_start_base_dir}")
|
||||||
|
else:
|
||||||
|
_warm_start_base_dir = opt.warm_start_base_dir
|
||||||
|
_output_folder_dir = opt.output_checkpoint_dir
|
||||||
|
|
||||||
|
continuous_binary_feat_list_save_path = os.path.join(
|
||||||
|
_warm_start_base_dir, "continuous_binary_feat_list.json"
|
||||||
|
)
|
||||||
|
|
||||||
|
if opt.old_feature_list != "none":
|
||||||
|
tf.logging.info("getting old continuous_binary_feat_list")
|
||||||
|
continuous_binary_feat_list = get_feature_list_for_heavy_ranking(
|
||||||
|
opt.old_feature_list, opt.data_spec
|
||||||
|
)
|
||||||
|
rmdir(continuous_binary_feat_list_save_path)
|
||||||
|
twml.util.write_file(
|
||||||
|
continuous_binary_feat_list_save_path, continuous_binary_feat_list, encode="json"
|
||||||
|
)
|
||||||
|
tf.logging.info(f"Finish writting files to {continuous_binary_feat_list_save_path}")
|
||||||
|
|
||||||
|
warm_start_folder = os.path.join(_warm_start_base_dir, "best_checkpoint")
|
||||||
|
if not tf.io.gfile.exists(warm_start_folder):
|
||||||
|
warm_start_folder = _warm_start_base_dir
|
||||||
|
|
||||||
|
rmdir(_output_folder_dir)
|
||||||
|
mkdirp(_output_folder_dir)
|
||||||
|
|
||||||
|
new_ckpt = warm_start_checkpoint(
|
||||||
|
warm_start_folder,
|
||||||
|
continuous_binary_feat_list_save_path,
|
||||||
|
feature_list_path,
|
||||||
|
opt.data_spec,
|
||||||
|
_output_folder_dir,
|
||||||
|
opt.model_type,
|
||||||
|
)
|
||||||
|
logging.info(f"Created new ckpt {new_ckpt} from {warm_start_folder}")
|
||||||
|
|
||||||
|
tf.logging.info("getting new continuous_binary_feat_list")
|
||||||
|
new_continuous_binary_feat_list_save_path = os.path.join(
|
||||||
|
_output_folder_dir, "continuous_binary_feat_list.json"
|
||||||
|
)
|
||||||
|
continuous_binary_feat_list = get_feature_list_for_heavy_ranking(
|
||||||
|
feature_list_path, opt.data_spec
|
||||||
|
)
|
||||||
|
rmdir(new_continuous_binary_feat_list_save_path)
|
||||||
|
twml.util.write_file(
|
||||||
|
new_continuous_binary_feat_list_save_path, continuous_binary_feat_list, encode="json"
|
||||||
|
)
|
||||||
|
tf.logging.info(f"Finish writting files to {new_continuous_binary_feat_list_save_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_main()
|
16
pushservice/src/main/python/models/libs/BUILD
Normal file
16
pushservice/src/main/python/models/libs/BUILD
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
python3_library(
|
||||||
|
name = "libs",
|
||||||
|
sources = ["*.py"],
|
||||||
|
tags = [
|
||||||
|
"bazel-compatible",
|
||||||
|
"no-mypy",
|
||||||
|
],
|
||||||
|
dependencies = [
|
||||||
|
"cortex/recsys/src/python/twitter/cortex/recsys/utils",
|
||||||
|
"magicpony/common/file_access/src/python/twitter/magicpony/common/file_access",
|
||||||
|
"src/python/twitter/cortex/ml/embeddings/deepbird",
|
||||||
|
"src/python/twitter/cortex/ml/embeddings/deepbird/grouped_metrics",
|
||||||
|
"src/python/twitter/deepbird/util/data",
|
||||||
|
"twml:twml-nodeps",
|
||||||
|
],
|
||||||
|
)
|
0
pushservice/src/main/python/models/libs/__init__.py
Normal file
0
pushservice/src/main/python/models/libs/__init__.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
# pylint: disable=no-member, arguments-differ, attribute-defined-outside-init, unused-argument
|
||||||
|
"""
|
||||||
|
Implementing Full Sparse Layer, allow specify use_binary_value in call() to
|
||||||
|
overide default action.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twml.layers import FullSparse as defaultFullSparse
|
||||||
|
from twml.layers.full_sparse import sparse_dense_matmul
|
||||||
|
|
||||||
|
import tensorflow.compat.v1 as tf
|
||||||
|
|
||||||
|
|
||||||
|
class FullSparse(defaultFullSparse):
|
||||||
|
def call(self, inputs, use_binary_values=None, **kwargs): # pylint: disable=unused-argument
|
||||||
|
"""The logic of the layer lives here.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
inputs:
|
||||||
|
A SparseTensor or a list of SparseTensors.
|
||||||
|
If `inputs` is a list, all tensors must have same `dense_shape`.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
- If `inputs` is `SparseTensor`, then returns `bias + inputs * dense_b`.
|
||||||
|
- If `inputs` is a `list[SparseTensor`, then returns
|
||||||
|
`bias + add_n([sp_a * dense_b for sp_a in inputs])`.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if use_binary_values is not None:
|
||||||
|
default_use_binary_values = use_binary_values
|
||||||
|
else:
|
||||||
|
default_use_binary_values = self.use_binary_values
|
||||||
|
|
||||||
|
if isinstance(default_use_binary_values, (list, tuple)):
|
||||||
|
raise ValueError(
|
||||||
|
"use_binary_values can not be %s when inputs is %s"
|
||||||
|
% (type(default_use_binary_values), type(inputs))
|
||||||
|
)
|
||||||
|
|
||||||
|
outputs = sparse_dense_matmul(
|
||||||
|
inputs,
|
||||||
|
self.weight,
|
||||||
|
self.use_sparse_grads,
|
||||||
|
default_use_binary_values,
|
||||||
|
name="sparse_mm",
|
||||||
|
partition_axis=self.partition_axis,
|
||||||
|
num_partitions=self.num_partitions,
|
||||||
|
compress_ids=self._use_compression,
|
||||||
|
cast_indices_dtype=self._cast_indices_dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.bias is not None:
|
||||||
|
outputs = tf.nn.bias_add(outputs, self.bias)
|
||||||
|
|
||||||
|
if self.activation is not None:
|
||||||
|
return self.activation(outputs) # pylint: disable=not-callable
|
||||||
|
return outputs
|
176
pushservice/src/main/python/models/libs/get_feat_config.py
Normal file
176
pushservice/src/main/python/models/libs/get_feat_config.py
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
from twitter.deepbird.projects.magic_recs.libs.metric_fn_utils import USER_AGE_FEATURE_NAME
|
||||||
|
from twitter.deepbird.projects.magic_recs.libs.model_utils import read_config
|
||||||
|
from twml.contrib import feature_config as contrib_feature_config
|
||||||
|
|
||||||
|
|
||||||
|
# checkstyle: noqa
|
||||||
|
|
||||||
|
FEAT_CONFIG_DEFAULT_VAL = -1.23456789
|
||||||
|
|
||||||
|
DEFAULT_INPUT_SIZE_BITS = 18
|
||||||
|
|
||||||
|
DEFAULT_FEATURE_LIST_PATH = "./feature_list_default.yaml"
|
||||||
|
FEATURE_LIST_DEFAULT_PATH = os.path.join(
|
||||||
|
os.path.dirname(os.path.realpath(__file__)), DEFAULT_FEATURE_LIST_PATH
|
||||||
|
)
|
||||||
|
|
||||||
|
DEFAULT_FEATURE_LIST_LIGHT_RANKING_PATH = "./feature_list_light_ranking.yaml"
|
||||||
|
FEATURE_LIST_DEFAULT_LIGHT_RANKING_PATH = os.path.join(
|
||||||
|
os.path.dirname(os.path.realpath(__file__)), DEFAULT_FEATURE_LIST_LIGHT_RANKING_PATH
|
||||||
|
)
|
||||||
|
|
||||||
|
FEATURE_LIST_DEFAULT = read_config(FEATURE_LIST_DEFAULT_PATH).items()
|
||||||
|
FEATURE_LIST_LIGHT_RANKING_DEFAULT = read_config(FEATURE_LIST_DEFAULT_LIGHT_RANKING_PATH).items()
|
||||||
|
|
||||||
|
|
||||||
|
LABELS = ["label"]
|
||||||
|
LABELS_MTL = {"OONC": ["label"], "OONC_Engagement": ["label", "label.engagement"]}
|
||||||
|
LABELS_LR = {
|
||||||
|
"Sent": ["label.sent"],
|
||||||
|
"HeavyRankPosition": ["meta.ranking.is_top3"],
|
||||||
|
"HeavyRankProbability": ["meta.ranking.weighted_oonc_model_score"],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _get_new_feature_config_base(
|
||||||
|
data_spec_path,
|
||||||
|
labels,
|
||||||
|
add_sparse_continous=True,
|
||||||
|
add_gbdt=True,
|
||||||
|
add_user_id=False,
|
||||||
|
add_timestamp=False,
|
||||||
|
add_user_age=False,
|
||||||
|
feature_list_provided=[],
|
||||||
|
opt=None,
|
||||||
|
run_light_ranking_group_metrics_in_bq=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Getter of the feature config based on specification.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
data_spec_path: A string indicating the path of the data_spec.json file, which could be
|
||||||
|
either a local path or a hdfs path.
|
||||||
|
labels: A list of strings indicating the name of the label in the data spec.
|
||||||
|
add_sparse_continous: A bool indicating if sparse_continuous feature needs to be included.
|
||||||
|
add_gbdt: A bool indicating if gbdt feature needs to be included.
|
||||||
|
add_user_id: A bool indicating if user_id feature needs to be included.
|
||||||
|
add_timestamp: A bool indicating if timestamp feature needs to be included. This will be useful
|
||||||
|
for sequential models and meta learning models.
|
||||||
|
add_user_age: A bool indicating if the user age feature needs to be included.
|
||||||
|
feature_list_provided: A list of features thats need to be included. If not specified, will use
|
||||||
|
FEATURE_LIST_DEFAULT by default.
|
||||||
|
opt: A namespace of arguments indicating the hyparameters.
|
||||||
|
run_light_ranking_group_metrics_in_bq: A bool indicating if heavy ranker score info needs to be included to compute group metrics in BigQuery.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A twml feature config object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
input_size_bits = DEFAULT_INPUT_SIZE_BITS if opt is None else opt.input_size_bits
|
||||||
|
|
||||||
|
feature_list = feature_list_provided if feature_list_provided != [] else FEATURE_LIST_DEFAULT
|
||||||
|
a_string_feat_list = [f[0] for f in feature_list if f[1] != "S"]
|
||||||
|
|
||||||
|
builder = contrib_feature_config.FeatureConfigBuilder(data_spec_path=data_spec_path)
|
||||||
|
|
||||||
|
builder = builder.extract_feature_group(
|
||||||
|
feature_regexes=a_string_feat_list,
|
||||||
|
group_name="continuous",
|
||||||
|
default_value=FEAT_CONFIG_DEFAULT_VAL,
|
||||||
|
type_filter=["CONTINUOUS"],
|
||||||
|
)
|
||||||
|
|
||||||
|
builder = builder.extract_features_as_hashed_sparse(
|
||||||
|
feature_regexes=a_string_feat_list,
|
||||||
|
output_tensor_name="sparse_no_continuous",
|
||||||
|
hash_space_size_bits=input_size_bits,
|
||||||
|
type_filter=["BINARY", "DISCRETE", "STRING", "SPARSE_BINARY"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if add_gbdt:
|
||||||
|
builder = builder.extract_features_as_hashed_sparse(
|
||||||
|
feature_regexes=["ads\..*"],
|
||||||
|
output_tensor_name="gbdt_sparse",
|
||||||
|
hash_space_size_bits=input_size_bits,
|
||||||
|
)
|
||||||
|
|
||||||
|
if add_sparse_continous:
|
||||||
|
s_string_feat_list = [f[0] for f in feature_list if f[1] == "S"]
|
||||||
|
|
||||||
|
builder = builder.extract_features_as_hashed_sparse(
|
||||||
|
feature_regexes=s_string_feat_list,
|
||||||
|
output_tensor_name="sparse_continuous",
|
||||||
|
hash_space_size_bits=input_size_bits,
|
||||||
|
type_filter=["SPARSE_CONTINUOUS"],
|
||||||
|
)
|
||||||
|
|
||||||
|
if add_user_id:
|
||||||
|
builder = builder.extract_feature("meta.user_id")
|
||||||
|
if add_timestamp:
|
||||||
|
builder = builder.extract_feature("meta.timestamp")
|
||||||
|
if add_user_age:
|
||||||
|
builder = builder.extract_feature(USER_AGE_FEATURE_NAME)
|
||||||
|
|
||||||
|
if run_light_ranking_group_metrics_in_bq:
|
||||||
|
builder = builder.extract_feature("meta.trace_id")
|
||||||
|
builder = builder.extract_feature("meta.ranking.weighted_oonc_model_score")
|
||||||
|
|
||||||
|
builder = builder.add_labels(labels).define_weight("meta.weight")
|
||||||
|
|
||||||
|
return builder.build()
|
||||||
|
|
||||||
|
|
||||||
|
def get_feature_config_with_sparse_continuous(
|
||||||
|
data_spec_path,
|
||||||
|
feature_list_provided=[],
|
||||||
|
opt=None,
|
||||||
|
add_user_id=False,
|
||||||
|
add_timestamp=False,
|
||||||
|
add_user_age=False,
|
||||||
|
):
|
||||||
|
task_name = opt.task_name if getattr(opt, "task_name", None) is not None else "OONC"
|
||||||
|
if task_name not in LABELS_MTL:
|
||||||
|
raise ValueError("Invalid Task Name !")
|
||||||
|
|
||||||
|
return _get_new_feature_config_base(
|
||||||
|
data_spec_path=data_spec_path,
|
||||||
|
labels=LABELS_MTL[task_name],
|
||||||
|
add_sparse_continous=True,
|
||||||
|
add_user_id=add_user_id,
|
||||||
|
add_timestamp=add_timestamp,
|
||||||
|
add_user_age=add_user_age,
|
||||||
|
feature_list_provided=feature_list_provided,
|
||||||
|
opt=opt,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_feature_config_light_ranking(
|
||||||
|
data_spec_path,
|
||||||
|
feature_list_provided=[],
|
||||||
|
opt=None,
|
||||||
|
add_user_id=True,
|
||||||
|
add_timestamp=False,
|
||||||
|
add_user_age=False,
|
||||||
|
add_gbdt=False,
|
||||||
|
run_light_ranking_group_metrics_in_bq=False,
|
||||||
|
):
|
||||||
|
task_name = opt.task_name if getattr(opt, "task_name", None) is not None else "HeavyRankPosition"
|
||||||
|
if task_name not in LABELS_LR:
|
||||||
|
raise ValueError("Invalid Task Name !")
|
||||||
|
if not feature_list_provided:
|
||||||
|
feature_list_provided = FEATURE_LIST_LIGHT_RANKING_DEFAULT
|
||||||
|
|
||||||
|
return _get_new_feature_config_base(
|
||||||
|
data_spec_path=data_spec_path,
|
||||||
|
labels=LABELS_LR[task_name],
|
||||||
|
add_sparse_continous=False,
|
||||||
|
add_gbdt=add_gbdt,
|
||||||
|
add_user_id=add_user_id,
|
||||||
|
add_timestamp=add_timestamp,
|
||||||
|
add_user_age=add_user_age,
|
||||||
|
feature_list_provided=feature_list_provided,
|
||||||
|
opt=opt,
|
||||||
|
run_light_ranking_group_metrics_in_bq=run_light_ranking_group_metrics_in_bq,
|
||||||
|
)
|
42
pushservice/src/main/python/models/libs/graph_utils.py
Normal file
42
pushservice/src/main/python/models/libs/graph_utils.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""
|
||||||
|
Utilties that aid in building the magic recs graph.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
import tensorflow.compat.v1 as tf
|
||||||
|
|
||||||
|
|
||||||
|
def get_trainable_variables(all_trainable_variables, trainable_regexes):
|
||||||
|
"""Returns a subset of trainable variables for training.
|
||||||
|
|
||||||
|
Given a collection of trainable variables, this will return all those that match the given regexes.
|
||||||
|
Will also log those variables.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
all_trainable_variables (a collection of trainable tf.Variable): The variables to search through.
|
||||||
|
trainable_regexes (a collection of regexes): Variables that match any regex will be included.
|
||||||
|
|
||||||
|
Returns a list of tf.Variable
|
||||||
|
"""
|
||||||
|
if trainable_regexes is None or len(trainable_regexes) == 0:
|
||||||
|
tf.logging.info("No trainable regexes found. Not using get_trainable_variables behavior.")
|
||||||
|
return None
|
||||||
|
|
||||||
|
assert any(
|
||||||
|
tf.is_tensor(var) for var in all_trainable_variables
|
||||||
|
), f"Non TF variable found: {all_trainable_variables}"
|
||||||
|
trainable_variables = list(
|
||||||
|
filter(
|
||||||
|
lambda var: any(re.match(regex, var.name, re.IGNORECASE) for regex in trainable_regexes),
|
||||||
|
all_trainable_variables,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
tf.logging.info(f"Using filtered trainable variables: {trainable_variables}")
|
||||||
|
|
||||||
|
assert (
|
||||||
|
trainable_variables
|
||||||
|
), "Did not find trainable variables after filtering after filtering from {} number of vars originaly. All vars: {} and train regexes: {}".format(
|
||||||
|
len(all_trainable_variables), all_trainable_variables, trainable_regexes
|
||||||
|
)
|
||||||
|
return trainable_variables
|
114
pushservice/src/main/python/models/libs/group_metrics.py
Normal file
114
pushservice/src/main/python/models/libs/group_metrics.py
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
|
from twitter.cortex.ml.embeddings.deepbird.grouped_metrics.computation import (
|
||||||
|
write_grouped_metrics_to_mldash,
|
||||||
|
)
|
||||||
|
from twitter.cortex.ml.embeddings.deepbird.grouped_metrics.configuration import (
|
||||||
|
ClassificationGroupedMetricsConfiguration,
|
||||||
|
NDCGGroupedMetricsConfiguration,
|
||||||
|
)
|
||||||
|
import twml
|
||||||
|
|
||||||
|
from .light_ranking_metrics import (
|
||||||
|
CGRGroupedMetricsConfiguration,
|
||||||
|
ExpectedLossGroupedMetricsConfiguration,
|
||||||
|
RecallGroupedMetricsConfiguration,
|
||||||
|
)
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import tensorflow.compat.v1 as tf
|
||||||
|
from tensorflow.compat.v1 import logging
|
||||||
|
|
||||||
|
|
||||||
|
# checkstyle: noqa
|
||||||
|
|
||||||
|
|
||||||
|
def run_group_metrics(trainer, data_dir, model_path, parse_fn, group_feature_name="meta.user_id"):
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
logging.info("Evaluating with group metrics.")
|
||||||
|
|
||||||
|
metrics = write_grouped_metrics_to_mldash(
|
||||||
|
trainer=trainer,
|
||||||
|
data_dir=data_dir,
|
||||||
|
model_path=model_path,
|
||||||
|
group_fn=lambda datarecord: str(
|
||||||
|
datarecord.discreteFeatures[twml.feature_id(group_feature_name)[0]]
|
||||||
|
),
|
||||||
|
parse_fn=parse_fn,
|
||||||
|
metric_configurations=[
|
||||||
|
ClassificationGroupedMetricsConfiguration(),
|
||||||
|
NDCGGroupedMetricsConfiguration(k=[5, 10, 20]),
|
||||||
|
],
|
||||||
|
total_records_to_read=1000000000,
|
||||||
|
shuffle=False,
|
||||||
|
mldash_metrics_name="grouped_metrics",
|
||||||
|
)
|
||||||
|
|
||||||
|
end_time = time.time()
|
||||||
|
logging.info(f"Evaluated Group Metics: {metrics}.")
|
||||||
|
logging.info(f"Group metrics evaluation time {end_time - start_time}.")
|
||||||
|
|
||||||
|
|
||||||
|
def run_group_metrics_light_ranking(
|
||||||
|
trainer, data_dir, model_path, parse_fn, group_feature_name="meta.trace_id"
|
||||||
|
):
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
logging.info("Evaluating with group metrics.")
|
||||||
|
|
||||||
|
metrics = write_grouped_metrics_to_mldash(
|
||||||
|
trainer=trainer,
|
||||||
|
data_dir=data_dir,
|
||||||
|
model_path=model_path,
|
||||||
|
group_fn=lambda datarecord: str(
|
||||||
|
datarecord.discreteFeatures[twml.feature_id(group_feature_name)[0]]
|
||||||
|
),
|
||||||
|
parse_fn=parse_fn,
|
||||||
|
metric_configurations=[
|
||||||
|
CGRGroupedMetricsConfiguration(lightNs=[50, 100, 200], heavyKs=[1, 3, 10, 20, 50]),
|
||||||
|
RecallGroupedMetricsConfiguration(n=[50, 100, 200], k=[1, 3, 10, 20, 50]),
|
||||||
|
ExpectedLossGroupedMetricsConfiguration(lightNs=[50, 100, 200]),
|
||||||
|
],
|
||||||
|
total_records_to_read=10000000,
|
||||||
|
num_batches_to_load=50,
|
||||||
|
batch_size=1024,
|
||||||
|
shuffle=False,
|
||||||
|
mldash_metrics_name="grouped_metrics_for_light_ranking",
|
||||||
|
)
|
||||||
|
|
||||||
|
end_time = time.time()
|
||||||
|
logging.info(f"Evaluated Group Metics for Light Ranking: {metrics}.")
|
||||||
|
logging.info(f"Group metrics evaluation time {end_time - start_time}.")
|
||||||
|
|
||||||
|
|
||||||
|
def run_group_metrics_light_ranking_in_bq(trainer, params, checkpoint_path):
|
||||||
|
logging.info("getting Test Predictions for Light Ranking Group Metrics in BigQuery !!!")
|
||||||
|
eval_input_fn = trainer.get_eval_input_fn(repeat=False, shuffle=False)
|
||||||
|
info_pool = []
|
||||||
|
|
||||||
|
for result in trainer.estimator.predict(
|
||||||
|
eval_input_fn, checkpoint_path=checkpoint_path, yield_single_examples=False
|
||||||
|
):
|
||||||
|
traceID = result["trace_id"]
|
||||||
|
pred = result["prediction"]
|
||||||
|
label = result["target"]
|
||||||
|
info = np.concatenate([traceID, pred, label], axis=1)
|
||||||
|
info_pool.append(info)
|
||||||
|
|
||||||
|
info_pool = np.concatenate(info_pool)
|
||||||
|
|
||||||
|
locname = "/tmp/000/"
|
||||||
|
if not os.path.exists(locname):
|
||||||
|
os.makedirs(locname)
|
||||||
|
|
||||||
|
locfile = locname + params.pred_file_name
|
||||||
|
columns = ["trace_id", "model_prediction", "meta__ranking__weighted_oonc_model_score"]
|
||||||
|
np.savetxt(locfile, info_pool, delimiter=",", header=",".join(columns))
|
||||||
|
tf.io.gfile.copy(locfile, params.pred_file_path + params.pred_file_name, overwrite=True)
|
||||||
|
|
||||||
|
if os.path.isfile(locfile):
|
||||||
|
os.remove(locfile)
|
||||||
|
|
||||||
|
logging.info("Done Prediction for Light Ranking Group Metrics in BigQuery.")
|
118
pushservice/src/main/python/models/libs/initializer.py
Normal file
118
pushservice/src/main/python/models/libs/initializer.py
Normal file
@ -0,0 +1,118 @@
|
|||||||
|
import numpy as np
|
||||||
|
from tensorflow.keras import backend as K
|
||||||
|
|
||||||
|
|
||||||
|
class VarianceScaling(object):
|
||||||
|
"""Initializer capable of adapting its scale to the shape of weights.
|
||||||
|
With `distribution="normal"`, samples are drawn from a truncated normal
|
||||||
|
distribution centered on zero, with `stddev = sqrt(scale / n)` where n is:
|
||||||
|
- number of input units in the weight tensor, if mode = "fan_in"
|
||||||
|
- number of output units, if mode = "fan_out"
|
||||||
|
- average of the numbers of input and output units, if mode = "fan_avg"
|
||||||
|
With `distribution="uniform"`,
|
||||||
|
samples are drawn from a uniform distribution
|
||||||
|
within [-limit, limit], with `limit = sqrt(3 * scale / n)`.
|
||||||
|
# Arguments
|
||||||
|
scale: Scaling factor (positive float).
|
||||||
|
mode: One of "fan_in", "fan_out", "fan_avg".
|
||||||
|
distribution: Random distribution to use. One of "normal", "uniform".
|
||||||
|
seed: A Python integer. Used to seed the random generator.
|
||||||
|
# Raises
|
||||||
|
ValueError: In case of an invalid value for the "scale", mode" or
|
||||||
|
"distribution" arguments."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
scale=1.0,
|
||||||
|
mode="fan_in",
|
||||||
|
distribution="normal",
|
||||||
|
seed=None,
|
||||||
|
fan_in=None,
|
||||||
|
fan_out=None,
|
||||||
|
):
|
||||||
|
self.fan_in = fan_in
|
||||||
|
self.fan_out = fan_out
|
||||||
|
if scale <= 0.0:
|
||||||
|
raise ValueError("`scale` must be a positive float. Got:", scale)
|
||||||
|
mode = mode.lower()
|
||||||
|
if mode not in {"fan_in", "fan_out", "fan_avg"}:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid `mode` argument: " 'expected on of {"fan_in", "fan_out", "fan_avg"} ' "but got",
|
||||||
|
mode,
|
||||||
|
)
|
||||||
|
distribution = distribution.lower()
|
||||||
|
if distribution not in {"normal", "uniform"}:
|
||||||
|
raise ValueError(
|
||||||
|
"Invalid `distribution` argument: " 'expected one of {"normal", "uniform"} ' "but got",
|
||||||
|
distribution,
|
||||||
|
)
|
||||||
|
self.scale = scale
|
||||||
|
self.mode = mode
|
||||||
|
self.distribution = distribution
|
||||||
|
self.seed = seed
|
||||||
|
|
||||||
|
def __call__(self, shape, dtype=None, partition_info=None):
|
||||||
|
fan_in = shape[-2] if self.fan_in is None else self.fan_in
|
||||||
|
fan_out = shape[-1] if self.fan_out is None else self.fan_out
|
||||||
|
|
||||||
|
scale = self.scale
|
||||||
|
if self.mode == "fan_in":
|
||||||
|
scale /= max(1.0, fan_in)
|
||||||
|
elif self.mode == "fan_out":
|
||||||
|
scale /= max(1.0, fan_out)
|
||||||
|
else:
|
||||||
|
scale /= max(1.0, float(fan_in + fan_out) / 2)
|
||||||
|
if self.distribution == "normal":
|
||||||
|
stddev = np.sqrt(scale) / 0.87962566103423978
|
||||||
|
return K.truncated_normal(shape, 0.0, stddev, dtype=dtype, seed=self.seed)
|
||||||
|
else:
|
||||||
|
limit = np.sqrt(3.0 * scale)
|
||||||
|
return K.random_uniform(shape, -limit, limit, dtype=dtype, seed=self.seed)
|
||||||
|
|
||||||
|
def get_config(self):
|
||||||
|
return {
|
||||||
|
"scale": self.scale,
|
||||||
|
"mode": self.mode,
|
||||||
|
"distribution": self.distribution,
|
||||||
|
"seed": self.seed,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def customized_glorot_uniform(seed=None, fan_in=None, fan_out=None):
|
||||||
|
"""Glorot uniform initializer, also called Xavier uniform initializer.
|
||||||
|
It draws samples from a uniform distribution within [-limit, limit]
|
||||||
|
where `limit` is `sqrt(6 / (fan_in + fan_out))`
|
||||||
|
where `fan_in` is the number of input units in the weight tensor
|
||||||
|
and `fan_out` is the number of output units in the weight tensor.
|
||||||
|
# Arguments
|
||||||
|
seed: A Python integer. Used to seed the random generator.
|
||||||
|
# Returns
|
||||||
|
An initializer."""
|
||||||
|
return VarianceScaling(
|
||||||
|
scale=1.0,
|
||||||
|
mode="fan_avg",
|
||||||
|
distribution="uniform",
|
||||||
|
seed=seed,
|
||||||
|
fan_in=fan_in,
|
||||||
|
fan_out=fan_out,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def customized_glorot_norm(seed=None, fan_in=None, fan_out=None):
|
||||||
|
"""Glorot norm initializer, also called Xavier uniform initializer.
|
||||||
|
It draws samples from a uniform distribution within [-limit, limit]
|
||||||
|
where `limit` is `sqrt(6 / (fan_in + fan_out))`
|
||||||
|
where `fan_in` is the number of input units in the weight tensor
|
||||||
|
and `fan_out` is the number of output units in the weight tensor.
|
||||||
|
# Arguments
|
||||||
|
seed: A Python integer. Used to seed the random generator.
|
||||||
|
# Returns
|
||||||
|
An initializer."""
|
||||||
|
return VarianceScaling(
|
||||||
|
scale=1.0,
|
||||||
|
mode="fan_avg",
|
||||||
|
distribution="normal",
|
||||||
|
seed=seed,
|
||||||
|
fan_in=fan_in,
|
||||||
|
fan_out=fan_out,
|
||||||
|
)
|
255
pushservice/src/main/python/models/libs/light_ranking_metrics.py
Normal file
255
pushservice/src/main/python/models/libs/light_ranking_metrics.py
Normal file
@ -0,0 +1,255 @@
|
|||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from twitter.cortex.ml.embeddings.deepbird.grouped_metrics.configuration import (
|
||||||
|
GroupedMetricsConfiguration,
|
||||||
|
)
|
||||||
|
from twitter.cortex.ml.embeddings.deepbird.grouped_metrics.helpers import (
|
||||||
|
extract_prediction_from_prediction_record,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# checkstyle: noqa
|
||||||
|
|
||||||
|
|
||||||
|
def score_loss_at_n(labels, predictions, lightN):
|
||||||
|
"""
|
||||||
|
Compute the absolute ScoreLoss ranking metric
|
||||||
|
Args:
|
||||||
|
labels (list) : A list of label values (HeavyRanking Reference)
|
||||||
|
predictions (list): A list of prediction values (LightRanking Predictions)
|
||||||
|
lightN (int): size of the list at which of Initial candidates to compute ScoreLoss. (LightRanking)
|
||||||
|
"""
|
||||||
|
assert len(labels) == len(predictions)
|
||||||
|
|
||||||
|
if lightN <= 0:
|
||||||
|
return None
|
||||||
|
|
||||||
|
labels_with_predictions = zip(labels, predictions)
|
||||||
|
labels_with_sorted_predictions = sorted(
|
||||||
|
labels_with_predictions, key=lambda x: x[1], reverse=True
|
||||||
|
)[:lightN]
|
||||||
|
labels_top1_light = max([label for label, _ in labels_with_sorted_predictions])
|
||||||
|
labels_top1_heavy = max(labels)
|
||||||
|
|
||||||
|
return labels_top1_heavy - labels_top1_light
|
||||||
|
|
||||||
|
|
||||||
|
def cgr_at_nk(labels, predictions, lightN, heavyK):
|
||||||
|
"""
|
||||||
|
Compute Cumulative Gain Ratio (CGR) ranking metric
|
||||||
|
Args:
|
||||||
|
labels (list) : A list of label values (HeavyRanking Reference)
|
||||||
|
predictions (list): A list of prediction values (LightRanking Predictions)
|
||||||
|
lightN (int): size of the list at which of Initial candidates to compute CGR. (LightRanking)
|
||||||
|
heavyK (int): size of the list at which of Refined candidates to compute CGR. (HeavyRanking)
|
||||||
|
"""
|
||||||
|
assert len(labels) == len(predictions)
|
||||||
|
|
||||||
|
if (not lightN) or (not heavyK):
|
||||||
|
out = None
|
||||||
|
elif lightN <= 0 or heavyK <= 0:
|
||||||
|
out = None
|
||||||
|
else:
|
||||||
|
|
||||||
|
labels_with_predictions = zip(labels, predictions)
|
||||||
|
labels_with_sorted_predictions = sorted(
|
||||||
|
labels_with_predictions, key=lambda x: x[1], reverse=True
|
||||||
|
)[:lightN]
|
||||||
|
labels_topN_light = [label for label, _ in labels_with_sorted_predictions]
|
||||||
|
|
||||||
|
if lightN <= heavyK:
|
||||||
|
cg_light = sum(labels_topN_light)
|
||||||
|
else:
|
||||||
|
labels_topK_heavy_from_light = sorted(labels_topN_light, reverse=True)[:heavyK]
|
||||||
|
cg_light = sum(labels_topK_heavy_from_light)
|
||||||
|
|
||||||
|
ideal_ordering = sorted(labels, reverse=True)
|
||||||
|
cg_heavy = sum(ideal_ordering[: min(lightN, heavyK)])
|
||||||
|
|
||||||
|
out = 0.0
|
||||||
|
if cg_heavy != 0:
|
||||||
|
out = max(cg_light / cg_heavy, 0)
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _get_weight(w, atK):
|
||||||
|
if not w:
|
||||||
|
return 1.0
|
||||||
|
elif len(w) <= atK:
|
||||||
|
return 0.0
|
||||||
|
else:
|
||||||
|
return w[atK]
|
||||||
|
|
||||||
|
|
||||||
|
def recall_at_nk(labels, predictions, n=None, k=None, w=None):
|
||||||
|
"""
|
||||||
|
Recall at N-K ranking metric
|
||||||
|
Args:
|
||||||
|
labels (list): A list of label values
|
||||||
|
predictions (list): A list of prediction values
|
||||||
|
n (int): size of the list at which of predictions to compute recall. (Light Ranking Predictions)
|
||||||
|
The default is None in which case the length of the provided predictions is used as L
|
||||||
|
k (int): size of the list at which of labels to compute recall. (Heavy Ranking Predictions)
|
||||||
|
The default is None in which case the length of the provided labels is used as L
|
||||||
|
w (list): weight vector sorted by labels
|
||||||
|
"""
|
||||||
|
assert len(labels) == len(predictions)
|
||||||
|
|
||||||
|
if not any(labels):
|
||||||
|
out = None
|
||||||
|
else:
|
||||||
|
|
||||||
|
safe_n = len(predictions) if not n else min(len(predictions), n)
|
||||||
|
safe_k = len(labels) if not k else min(len(labels), k)
|
||||||
|
|
||||||
|
labels_with_predictions = zip(labels, predictions)
|
||||||
|
sorted_labels_with_predictions = sorted(
|
||||||
|
labels_with_predictions, key=lambda x: x[0], reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
order_sorted_labels_predictions = zip(range(len(labels)), *zip(*sorted_labels_with_predictions))
|
||||||
|
|
||||||
|
order_with_predictions = [
|
||||||
|
(order, pred) for order, label, pred in order_sorted_labels_predictions
|
||||||
|
]
|
||||||
|
order_with_sorted_predictions = sorted(order_with_predictions, key=lambda x: x[1], reverse=True)
|
||||||
|
|
||||||
|
pred_sorted_order_at_n = [order for order, _ in order_with_sorted_predictions][:safe_n]
|
||||||
|
|
||||||
|
intersection_weight = [
|
||||||
|
_get_weight(w, order) if order < safe_k else 0 for order in pred_sorted_order_at_n
|
||||||
|
]
|
||||||
|
|
||||||
|
intersection_score = sum(intersection_weight)
|
||||||
|
full_score = sum(w) if w else float(safe_k)
|
||||||
|
|
||||||
|
out = 0.0
|
||||||
|
if full_score != 0:
|
||||||
|
out = intersection_score / full_score
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
class ExpectedLossGroupedMetricsConfiguration(GroupedMetricsConfiguration):
|
||||||
|
"""
|
||||||
|
This is the Expected Loss Grouped metric computation configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, lightNs=[]):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
lightNs (list): size of the list at which of Initial candidates to compute Expected Loss. (LightRanking)
|
||||||
|
"""
|
||||||
|
self.lightNs = lightNs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return "ExpectedLoss"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metrics_dict(self):
|
||||||
|
metrics_to_compute = {}
|
||||||
|
for lightN in self.lightNs:
|
||||||
|
metric_name = "ExpectedLoss_atLight_" + str(lightN)
|
||||||
|
metrics_to_compute[metric_name] = partial(score_loss_at_n, lightN=lightN)
|
||||||
|
return metrics_to_compute
|
||||||
|
|
||||||
|
def extract_label(self, prec, drec, drec_label):
|
||||||
|
return drec_label
|
||||||
|
|
||||||
|
def extract_prediction(self, prec, drec, drec_label):
|
||||||
|
return extract_prediction_from_prediction_record(prec)
|
||||||
|
|
||||||
|
|
||||||
|
class CGRGroupedMetricsConfiguration(GroupedMetricsConfiguration):
|
||||||
|
"""
|
||||||
|
This is the Cumulative Gain Ratio (CGR) Grouped metric computation configuration.
|
||||||
|
CGR at the max length of each session is the default.
|
||||||
|
CGR at additional positions can be computed by specifying a list of 'n's and 'k's
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, lightNs=[], heavyKs=[]):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
lightNs (list): size of the list at which of Initial candidates to compute CGR. (LightRanking)
|
||||||
|
heavyK (int): size of the list at which of Refined candidates to compute CGR. (HeavyRanking)
|
||||||
|
"""
|
||||||
|
self.lightNs = lightNs
|
||||||
|
self.heavyKs = heavyKs
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return "cgr"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metrics_dict(self):
|
||||||
|
metrics_to_compute = {}
|
||||||
|
for lightN in self.lightNs:
|
||||||
|
for heavyK in self.heavyKs:
|
||||||
|
metric_name = "cgr_atLight_" + str(lightN) + "_atHeavy_" + str(heavyK)
|
||||||
|
metrics_to_compute[metric_name] = partial(cgr_at_nk, lightN=lightN, heavyK=heavyK)
|
||||||
|
return metrics_to_compute
|
||||||
|
|
||||||
|
def extract_label(self, prec, drec, drec_label):
|
||||||
|
return drec_label
|
||||||
|
|
||||||
|
def extract_prediction(self, prec, drec, drec_label):
|
||||||
|
return extract_prediction_from_prediction_record(prec)
|
||||||
|
|
||||||
|
|
||||||
|
class RecallGroupedMetricsConfiguration(GroupedMetricsConfiguration):
|
||||||
|
"""
|
||||||
|
This is the Recall Grouped metric computation configuration.
|
||||||
|
Recall at the max length of each session is the default.
|
||||||
|
Recall at additional positions can be computed by specifying a list of 'n's and 'k's
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, n=[], k=[], w=[]):
|
||||||
|
"""
|
||||||
|
Args:
|
||||||
|
n (list): A list of ints. List of prediction rank thresholds (for light)
|
||||||
|
k (list): A list of ints. List of label rank thresholds (for heavy)
|
||||||
|
"""
|
||||||
|
self.predN = n
|
||||||
|
self.labelK = k
|
||||||
|
self.weight = w
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return "group_recall"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def metrics_dict(self):
|
||||||
|
metrics_to_compute = {"group_recall_unweighted": recall_at_nk}
|
||||||
|
if not self.weight:
|
||||||
|
metrics_to_compute["group_recall_weighted"] = partial(recall_at_nk, w=self.weight)
|
||||||
|
|
||||||
|
if self.predN and self.labelK:
|
||||||
|
for n in self.predN:
|
||||||
|
for k in self.labelK:
|
||||||
|
if n >= k:
|
||||||
|
metrics_to_compute[
|
||||||
|
"group_recall_unweighted_at_L" + str(n) + "_at_H" + str(k)
|
||||||
|
] = partial(recall_at_nk, n=n, k=k)
|
||||||
|
if self.weight:
|
||||||
|
metrics_to_compute[
|
||||||
|
"group_recall_weighted_at_L" + str(n) + "_at_H" + str(k)
|
||||||
|
] = partial(recall_at_nk, n=n, k=k, w=self.weight)
|
||||||
|
|
||||||
|
if self.labelK and not self.predN:
|
||||||
|
for k in self.labelK:
|
||||||
|
metrics_to_compute["group_recall_unweighted_at_full_at_H" + str(k)] = partial(
|
||||||
|
recall_at_nk, k=k
|
||||||
|
)
|
||||||
|
if self.weight:
|
||||||
|
metrics_to_compute["group_recall_weighted_at_full_at_H" + str(k)] = partial(
|
||||||
|
recall_at_nk, k=k, w=self.weight
|
||||||
|
)
|
||||||
|
return metrics_to_compute
|
||||||
|
|
||||||
|
def extract_label(self, prec, drec, drec_label):
|
||||||
|
return drec_label
|
||||||
|
|
||||||
|
def extract_prediction(self, prec, drec, drec_label):
|
||||||
|
return extract_prediction_from_prediction_record(prec)
|
294
pushservice/src/main/python/models/libs/metric_fn_utils.py
Normal file
294
pushservice/src/main/python/models/libs/metric_fn_utils.py
Normal file
@ -0,0 +1,294 @@
|
|||||||
|
"""
|
||||||
|
Utilties for constructing a metric_fn for magic recs.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from twml.contrib.metrics.metrics import (
|
||||||
|
get_dual_binary_tasks_metric_fn,
|
||||||
|
get_numeric_metric_fn,
|
||||||
|
get_partial_multi_binary_class_metric_fn,
|
||||||
|
get_single_binary_task_metric_fn,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .model_utils import generate_disliked_mask
|
||||||
|
|
||||||
|
import tensorflow.compat.v1 as tf
|
||||||
|
|
||||||
|
|
||||||
|
METRIC_BOOK = {
|
||||||
|
"OONC": ["OONC"],
|
||||||
|
"OONC_Engagement": ["OONC", "Engagement"],
|
||||||
|
"Sent": ["Sent"],
|
||||||
|
"HeavyRankPosition": ["HeavyRankPosition"],
|
||||||
|
"HeavyRankProbability": ["HeavyRankProbability"],
|
||||||
|
}
|
||||||
|
|
||||||
|
USER_AGE_FEATURE_NAME = "accountAge"
|
||||||
|
NEW_USER_AGE_CUTOFF = 0
|
||||||
|
|
||||||
|
|
||||||
|
def remove_padding_and_flatten(tensor, valid_batch_size):
|
||||||
|
"""Remove the padding of the input padded tensor given the valid batch size tensor,
|
||||||
|
then flatten the output with respect to the first dimension.
|
||||||
|
Args:
|
||||||
|
tensor: A tensor of size [META_BATCH_SIZE, BATCH_SIZE, FEATURE_DIM].
|
||||||
|
valid_batch_size: A tensor of size [META_BATCH_SIZE], with each element indicating
|
||||||
|
the effective batch size of the BATCH_SIZE dimension.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A tesnor of size [tf.reduce_sum(valid_batch_size), FEATURE_DIM].
|
||||||
|
"""
|
||||||
|
unpadded_ragged_tensor = tf.RaggedTensor.from_tensor(tensor=tensor, lengths=valid_batch_size)
|
||||||
|
|
||||||
|
return unpadded_ragged_tensor.flat_values
|
||||||
|
|
||||||
|
|
||||||
|
def safe_mask(values, mask):
|
||||||
|
"""Mask values if possible.
|
||||||
|
|
||||||
|
Boolean mask inputed values if and only if values is a tensor of the same dimension as mask (or can be broadcasted to that dimension).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
values (Any or Tensor): Input tensor to mask. Dim 0 should be size N.
|
||||||
|
mask (boolean tensor): A boolean tensor of size N.
|
||||||
|
|
||||||
|
Returns Values or Values masked.
|
||||||
|
"""
|
||||||
|
if values is None:
|
||||||
|
return values
|
||||||
|
if not tf.is_tensor(values):
|
||||||
|
return values
|
||||||
|
values_shape = values.get_shape()
|
||||||
|
if not values_shape or len(values_shape) == 0:
|
||||||
|
return values
|
||||||
|
if not mask.get_shape().is_compatible_with(values_shape[0]):
|
||||||
|
return values
|
||||||
|
return tf.boolean_mask(values, mask)
|
||||||
|
|
||||||
|
|
||||||
|
def add_new_user_metrics(metric_fn):
|
||||||
|
"""Will stratify the metric_fn by adding new user metrics.
|
||||||
|
|
||||||
|
Given an input metric_fn, double every metric: One will be the orignal and the other will only include those for new users.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metric_fn (python function): Base twml metric_fn.
|
||||||
|
|
||||||
|
Returns a metric_fn with new user metrics included.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def metric_fn_with_new_users(graph_output, labels, weights):
|
||||||
|
if USER_AGE_FEATURE_NAME not in graph_output:
|
||||||
|
raise ValueError(
|
||||||
|
"In order to get metrics stratified by user age, {name} feature should be added to model graph output. However, only the following output keys were found: {keys}.".format(
|
||||||
|
name=USER_AGE_FEATURE_NAME, keys=graph_output.keys()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
metric_ops = metric_fn(graph_output, labels, weights)
|
||||||
|
|
||||||
|
is_new = tf.reshape(
|
||||||
|
tf.math.less_equal(
|
||||||
|
tf.cast(graph_output[USER_AGE_FEATURE_NAME], tf.int64),
|
||||||
|
tf.cast(NEW_USER_AGE_CUTOFF, tf.int64),
|
||||||
|
),
|
||||||
|
[-1],
|
||||||
|
)
|
||||||
|
|
||||||
|
labels = safe_mask(labels, is_new)
|
||||||
|
weights = safe_mask(weights, is_new)
|
||||||
|
graph_output = {key: safe_mask(values, is_new) for key, values in graph_output.items()}
|
||||||
|
|
||||||
|
new_user_metric_ops = metric_fn(graph_output, labels, weights)
|
||||||
|
new_user_metric_ops = {name + "_new_users": ops for name, ops in new_user_metric_ops.items()}
|
||||||
|
metric_ops.update(new_user_metric_ops)
|
||||||
|
return metric_ops
|
||||||
|
|
||||||
|
return metric_fn_with_new_users
|
||||||
|
|
||||||
|
|
||||||
|
def get_meta_learn_single_binary_task_metric_fn(
|
||||||
|
metrics, classnames, top_k=(5, 5, 5), use_top_k=False
|
||||||
|
):
|
||||||
|
"""Wrapper function to use the metric_fn with meta learning evaluation scheme.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metrics: A list of string representing metric names.
|
||||||
|
classnames: A list of string repsenting class names, In case of multiple binary class models,
|
||||||
|
the names for each class or label.
|
||||||
|
top_k: A tuple of int to specify top K metrics.
|
||||||
|
use_top_k: A boolean value indicating of top K of metrics is used.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A customized metric_fn function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_eval_metric_ops(graph_output, labels, weights):
|
||||||
|
"""The op func of the eval_metrics. Comparing with normal version,
|
||||||
|
the difference is we flatten the output, label, and weights.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
graph_output: A dict of tensors.
|
||||||
|
labels: A tensor of int32 be the value of either 0 or 1.
|
||||||
|
weights: A tensor of float32 to indicate the per record weight.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict of metric names and values.
|
||||||
|
"""
|
||||||
|
metric_op_weighted = get_partial_multi_binary_class_metric_fn(
|
||||||
|
metrics, predcols=0, classes=classnames
|
||||||
|
)
|
||||||
|
classnames_unweighted = ["unweighted_" + classname for classname in classnames]
|
||||||
|
metric_op_unweighted = get_partial_multi_binary_class_metric_fn(
|
||||||
|
metrics, predcols=0, classes=classnames_unweighted
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_batch_size = graph_output["valid_batch_size"]
|
||||||
|
graph_output["output"] = remove_padding_and_flatten(graph_output["output"], valid_batch_size)
|
||||||
|
labels = remove_padding_and_flatten(labels, valid_batch_size)
|
||||||
|
weights = remove_padding_and_flatten(weights, valid_batch_size)
|
||||||
|
|
||||||
|
tf.ensure_shape(graph_output["output"], [None, 1])
|
||||||
|
tf.ensure_shape(labels, [None, 1])
|
||||||
|
tf.ensure_shape(weights, [None, 1])
|
||||||
|
|
||||||
|
metrics_weighted = metric_op_weighted(graph_output, labels, weights)
|
||||||
|
metrics_unweighted = metric_op_unweighted(graph_output, labels, None)
|
||||||
|
metrics_weighted.update(metrics_unweighted)
|
||||||
|
|
||||||
|
if use_top_k:
|
||||||
|
metric_op_numeric = get_numeric_metric_fn(metrics=None, topK=top_k, predcol=0, labelcol=1)
|
||||||
|
metrics_numeric = metric_op_numeric(graph_output, labels, weights)
|
||||||
|
metrics_weighted.update(metrics_numeric)
|
||||||
|
return metrics_weighted
|
||||||
|
|
||||||
|
return get_eval_metric_ops
|
||||||
|
|
||||||
|
|
||||||
|
def get_meta_learn_dual_binary_tasks_metric_fn(
|
||||||
|
metrics, classnames, top_k=(5, 5, 5), use_top_k=False
|
||||||
|
):
|
||||||
|
"""Wrapper function to use the metric_fn with meta learning evaluation scheme.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
metrics: A list of string representing metric names.
|
||||||
|
classnames: A list of string repsenting class names, In case of multiple binary class models,
|
||||||
|
the names for each class or label.
|
||||||
|
top_k: A tuple of int to specify top K metrics.
|
||||||
|
use_top_k: A boolean value indicating of top K of metrics is used.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A customized metric_fn function.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_eval_metric_ops(graph_output, labels, weights):
|
||||||
|
"""The op func of the eval_metrics. Comparing with normal version,
|
||||||
|
the difference is we flatten the output, label, and weights.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
graph_output: A dict of tensors.
|
||||||
|
labels: A tensor of int32 be the value of either 0 or 1.
|
||||||
|
weights: A tensor of float32 to indicate the per record weight.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict of metric names and values.
|
||||||
|
"""
|
||||||
|
metric_op_weighted = get_partial_multi_binary_class_metric_fn(
|
||||||
|
metrics, predcols=[0, 1], classes=classnames
|
||||||
|
)
|
||||||
|
classnames_unweighted = ["unweighted_" + classname for classname in classnames]
|
||||||
|
metric_op_unweighted = get_partial_multi_binary_class_metric_fn(
|
||||||
|
metrics, predcols=[0, 1], classes=classnames_unweighted
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_batch_size = graph_output["valid_batch_size"]
|
||||||
|
graph_output["output"] = remove_padding_and_flatten(graph_output["output"], valid_batch_size)
|
||||||
|
labels = remove_padding_and_flatten(labels, valid_batch_size)
|
||||||
|
weights = remove_padding_and_flatten(weights, valid_batch_size)
|
||||||
|
|
||||||
|
tf.ensure_shape(graph_output["output"], [None, 2])
|
||||||
|
tf.ensure_shape(labels, [None, 2])
|
||||||
|
tf.ensure_shape(weights, [None, 1])
|
||||||
|
|
||||||
|
metrics_weighted = metric_op_weighted(graph_output, labels, weights)
|
||||||
|
metrics_unweighted = metric_op_unweighted(graph_output, labels, None)
|
||||||
|
metrics_weighted.update(metrics_unweighted)
|
||||||
|
|
||||||
|
if use_top_k:
|
||||||
|
metric_op_numeric = get_numeric_metric_fn(metrics=None, topK=top_k, predcol=2, labelcol=2)
|
||||||
|
metrics_numeric = metric_op_numeric(graph_output, labels, weights)
|
||||||
|
metrics_weighted.update(metrics_numeric)
|
||||||
|
return metrics_weighted
|
||||||
|
|
||||||
|
return get_eval_metric_ops
|
||||||
|
|
||||||
|
|
||||||
|
def get_metric_fn(task_name, use_stratify_metrics, use_meta_batch=False):
|
||||||
|
"""Will retrieve the metric_fn for magic recs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
task_name (string): Which task is being used for this model.
|
||||||
|
use_stratify_metrics (boolean): Should we add stratified metrics (new user metrics).
|
||||||
|
use_meta_batch (boolean): If the output/label/weights are passed in 3D shape instead of
|
||||||
|
2D shape.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A metric_fn function to pass in twml Trainer.
|
||||||
|
"""
|
||||||
|
if task_name not in METRIC_BOOK:
|
||||||
|
raise ValueError(
|
||||||
|
"Task name of {task_name} not recognized. Unable to retrieve metrics.".format(
|
||||||
|
task_name=task_name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
class_names = METRIC_BOOK[task_name]
|
||||||
|
if use_meta_batch:
|
||||||
|
get_n_binary_task_metric_fn = (
|
||||||
|
get_meta_learn_single_binary_task_metric_fn
|
||||||
|
if len(class_names) == 1
|
||||||
|
else get_meta_learn_dual_binary_tasks_metric_fn
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
get_n_binary_task_metric_fn = (
|
||||||
|
get_single_binary_task_metric_fn if len(class_names) == 1 else get_dual_binary_tasks_metric_fn
|
||||||
|
)
|
||||||
|
|
||||||
|
metric_fn = get_n_binary_task_metric_fn(metrics=None, classnames=METRIC_BOOK[task_name])
|
||||||
|
|
||||||
|
if use_stratify_metrics:
|
||||||
|
metric_fn = add_new_user_metrics(metric_fn)
|
||||||
|
|
||||||
|
return metric_fn
|
||||||
|
|
||||||
|
|
||||||
|
def flip_disliked_labels(metric_fn):
|
||||||
|
"""This function returns an adapted metric_fn which flips the labels of the OONCed evaluation data to 0 if it is disliked.
|
||||||
|
Args:
|
||||||
|
metric_fn: A metric_fn function to pass in twml Trainer.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
_adapted_metric_fn: A customized metric_fn function with disliked OONC labels flipped.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _adapted_metric_fn(graph_output, labels, weights):
|
||||||
|
"""A customized metric_fn function with disliked OONC labels flipped.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
graph_output: A dict of tensors.
|
||||||
|
labels: labels of training samples, which is a 2D tensor of shape batch_size x 3: [OONCs, engagements, dislikes]
|
||||||
|
weights: A tensor of float32 to indicate the per record weight.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict of metric names and values.
|
||||||
|
"""
|
||||||
|
# We want to multiply the label of the observation by 0 only when it is disliked
|
||||||
|
disliked_mask = generate_disliked_mask(labels)
|
||||||
|
|
||||||
|
# Extract OONC and engagement labels only.
|
||||||
|
labels = tf.reshape(labels[:, 0:2], shape=[-1, 2])
|
||||||
|
|
||||||
|
# Labels will be set to 0 if it is disliked.
|
||||||
|
adapted_labels = labels * tf.cast(tf.logical_not(disliked_mask), dtype=labels.dtype)
|
||||||
|
|
||||||
|
return metric_fn(graph_output, adapted_labels, weights)
|
||||||
|
|
||||||
|
return _adapted_metric_fn
|
231
pushservice/src/main/python/models/libs/model_args.py
Normal file
231
pushservice/src/main/python/models/libs/model_args.py
Normal file
@ -0,0 +1,231 @@
|
|||||||
|
from twml.trainers import DataRecordTrainer
|
||||||
|
|
||||||
|
|
||||||
|
# checkstyle: noqa
|
||||||
|
|
||||||
|
|
||||||
|
def get_arg_parser():
|
||||||
|
parser = DataRecordTrainer.add_parser_arguments()
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--input_size_bits",
|
||||||
|
type=int,
|
||||||
|
default=18,
|
||||||
|
help="number of bits allocated to the input size",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--model_trainer_name",
|
||||||
|
default="magic_recs_mlp_calibration_MTL_OONC_Engagement",
|
||||||
|
type=str,
|
||||||
|
help="specify the model trainer name.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--model_type",
|
||||||
|
default="deepnorm_gbdt_inputdrop2_rescale",
|
||||||
|
type=str,
|
||||||
|
help="specify the model type to use.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--feat_config_type",
|
||||||
|
default="get_feature_config_with_sparse_continuous",
|
||||||
|
type=str,
|
||||||
|
help="specify the feature configure function to use.",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--directly_export_best",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="whether to directly_export best_checkpoint",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--warm_start_base_dir",
|
||||||
|
default="none",
|
||||||
|
type=str,
|
||||||
|
help="latest ckpt in this folder will be used to ",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--feature_list",
|
||||||
|
default="none",
|
||||||
|
type=str,
|
||||||
|
help="Which features to use for training",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--warm_start_from", default=None, type=str, help="model dir to warm start from"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--momentum", default=0.99999, type=float, help="Momentum term for batch normalization"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dropout",
|
||||||
|
default=0.2,
|
||||||
|
type=float,
|
||||||
|
help="input_dropout_rate to rescale output by (1 - input_dropout_rate)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--out_layer_1_size", default=256, type=int, help="Size of MLP_branch layer 1"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--out_layer_2_size", default=128, type=int, help="Size of MLP_branch layer 2"
|
||||||
|
)
|
||||||
|
parser.add_argument("--out_layer_3_size", default=64, type=int, help="Size of MLP_branch layer 3")
|
||||||
|
parser.add_argument(
|
||||||
|
"--sparse_embedding_size", default=50, type=int, help="Dimensionality of sparse embedding layer"
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dense_embedding_size", default=128, type=int, help="Dimensionality of dense embedding layer"
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--use_uam_label",
|
||||||
|
default=False,
|
||||||
|
type=str,
|
||||||
|
help="Whether to use uam_label or not",
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--task_name",
|
||||||
|
default="OONC_Engagement",
|
||||||
|
type=str,
|
||||||
|
help="specify the task name to use: OONC or OONC_Engagement.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--init_weight",
|
||||||
|
default=0.9,
|
||||||
|
type=float,
|
||||||
|
help="Initial OONC Task Weight MTL: OONC+Engagement.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--use_engagement_weight",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="whether to use engagement weight for base model.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--mtl_num_extra_layers",
|
||||||
|
type=int,
|
||||||
|
default=1,
|
||||||
|
help="Number of Hidden Layers for each TaskBranch.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--mtl_neuron_scale", type=int, default=4, help="Scaling Factor of Neurons in MTL Extra Layers."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--use_oonc_score",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="whether to use oonc score only or combined score.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--use_stratified_metrics",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Use stratified metrics: Break out new-user metrics.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--run_group_metrics",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Will run evaluation metrics grouped by user.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--use_full_scope",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Will add extra scope and naming to graph.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--trainable_regexes",
|
||||||
|
default=None,
|
||||||
|
nargs="*",
|
||||||
|
help="The union of variables specified by the list of regexes will be considered trainable.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--fine_tuning.ckpt_to_initialize_from",
|
||||||
|
dest="fine_tuning_ckpt_to_initialize_from",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="Checkpoint path from which to warm start. Indicates the pre-trained model.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--fine_tuning.warm_start_scope_regex",
|
||||||
|
dest="fine_tuning_warm_start_scope_regex",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="All variables matching this will be restored.",
|
||||||
|
)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def get_params(args=None):
|
||||||
|
parser = get_arg_parser()
|
||||||
|
if args is None:
|
||||||
|
return parser.parse_args()
|
||||||
|
else:
|
||||||
|
return parser.parse_args(args)
|
||||||
|
|
||||||
|
|
||||||
|
def get_arg_parser_light_ranking():
|
||||||
|
parser = get_arg_parser()
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--use_record_weight",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="whether to use record weight for base model.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--min_record_weight", default=0.0, type=float, help="Minimum record weight to use."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--smooth_weight", default=0.0, type=float, help="Factor to smooth Rank Position Weight."
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument(
|
||||||
|
"--num_mlp_layers", type=int, default=3, help="Number of Hidden Layers for MLP model."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--mlp_neuron_scale", type=int, default=4, help="Scaling Factor of Neurons in MLP Layers."
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--run_light_ranking_group_metrics",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Will run evaluation metrics grouped by user for Light Ranking.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--use_missing_sub_branch",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Whether to use missing value sub-branch for Light Ranking.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--use_gbdt_features",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Whether to use GBDT features for Light Ranking.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--run_light_ranking_group_metrics_in_bq",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Whether to get_predictions for Light Ranking to compute group metrics in BigQuery.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--pred_file_path",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="path",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--pred_file_name",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="path",
|
||||||
|
)
|
||||||
|
return parser
|
339
pushservice/src/main/python/models/libs/model_utils.py
Normal file
339
pushservice/src/main/python/models/libs/model_utils.py
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
import sys
|
||||||
|
|
||||||
|
import twml
|
||||||
|
|
||||||
|
from .initializer import customized_glorot_uniform
|
||||||
|
|
||||||
|
import tensorflow.compat.v1 as tf
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
|
||||||
|
# checkstyle: noqa
|
||||||
|
|
||||||
|
|
||||||
|
def read_config(whitelist_yaml_file):
|
||||||
|
with tf.gfile.FastGFile(whitelist_yaml_file) as f:
|
||||||
|
try:
|
||||||
|
return yaml.safe_load(f)
|
||||||
|
except yaml.YAMLError as exc:
|
||||||
|
print(exc)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def _sparse_feature_fixup(features, input_size_bits):
|
||||||
|
"""Rebuild a sparse tensor feature so that its dense shape attribute is present.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
features (SparseTensor): Sparse feature tensor of shape ``(B, sparse_feature_dim)``.
|
||||||
|
input_size_bits (int): Number of columns in ``log2`` scale. Must be positive.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
SparseTensor: Rebuilt and non-faulty version of `features`."""
|
||||||
|
sparse_feature_dim = tf.constant(2**input_size_bits, dtype=tf.int64)
|
||||||
|
sparse_shape = tf.stack([features.dense_shape[0], sparse_feature_dim])
|
||||||
|
sparse_tf = tf.SparseTensor(features.indices, features.values, sparse_shape)
|
||||||
|
return sparse_tf
|
||||||
|
|
||||||
|
|
||||||
|
def self_atten_dense(input, out_dim, activation=None, use_bias=True, name=None):
|
||||||
|
def safe_concat(base, suffix):
|
||||||
|
"""Concats variables name components if base is given."""
|
||||||
|
if not base:
|
||||||
|
return base
|
||||||
|
return f"{base}:{suffix}"
|
||||||
|
|
||||||
|
input_dim = input.shape.as_list()[1]
|
||||||
|
|
||||||
|
sigmoid_out = twml.layers.FullDense(
|
||||||
|
input_dim, dtype=tf.float32, activation=tf.nn.sigmoid, name=safe_concat(name, "sigmoid_out")
|
||||||
|
)(input)
|
||||||
|
atten_input = sigmoid_out * input
|
||||||
|
mlp_out = twml.layers.FullDense(
|
||||||
|
out_dim,
|
||||||
|
dtype=tf.float32,
|
||||||
|
activation=activation,
|
||||||
|
use_bias=use_bias,
|
||||||
|
name=safe_concat(name, "mlp_out"),
|
||||||
|
)(atten_input)
|
||||||
|
return mlp_out
|
||||||
|
|
||||||
|
|
||||||
|
def get_dense_out(input, out_dim, activation, dense_type):
|
||||||
|
if dense_type == "full_dense":
|
||||||
|
out = twml.layers.FullDense(out_dim, dtype=tf.float32, activation=activation)(input)
|
||||||
|
elif dense_type == "self_atten_dense":
|
||||||
|
out = self_atten_dense(input, out_dim, activation=activation)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def get_input_trans_func(bn_normalized_dense, is_training):
|
||||||
|
gw_normalized_dense = tf.expand_dims(bn_normalized_dense, -1)
|
||||||
|
group_num = bn_normalized_dense.shape.as_list()[1]
|
||||||
|
|
||||||
|
gw_normalized_dense = GroupWiseTrans(group_num, 1, 8, name="groupwise_1", activation=tf.tanh)(
|
||||||
|
gw_normalized_dense
|
||||||
|
)
|
||||||
|
gw_normalized_dense = GroupWiseTrans(group_num, 8, 4, name="groupwise_2", activation=tf.tanh)(
|
||||||
|
gw_normalized_dense
|
||||||
|
)
|
||||||
|
gw_normalized_dense = GroupWiseTrans(group_num, 4, 1, name="groupwise_3", activation=tf.tanh)(
|
||||||
|
gw_normalized_dense
|
||||||
|
)
|
||||||
|
|
||||||
|
gw_normalized_dense = tf.squeeze(gw_normalized_dense, [-1])
|
||||||
|
|
||||||
|
bn_gw_normalized_dense = tf.layers.batch_normalization(
|
||||||
|
gw_normalized_dense,
|
||||||
|
training=is_training,
|
||||||
|
renorm_momentum=0.9999,
|
||||||
|
momentum=0.9999,
|
||||||
|
renorm=is_training,
|
||||||
|
trainable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return bn_gw_normalized_dense
|
||||||
|
|
||||||
|
|
||||||
|
def tensor_dropout(
|
||||||
|
input_tensor,
|
||||||
|
rate,
|
||||||
|
is_training,
|
||||||
|
sparse_tensor=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Implements dropout layer for both dense and sparse input_tensor
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
input_tensor:
|
||||||
|
B x D dense tensor, or a sparse tensor
|
||||||
|
rate (float32):
|
||||||
|
dropout rate
|
||||||
|
is_training (bool):
|
||||||
|
training stage or not.
|
||||||
|
sparse_tensor (bool):
|
||||||
|
whether the input_tensor is sparse tensor or not. Default to be None, this value has to be passed explicitly.
|
||||||
|
rescale_sparse_dropout (bool):
|
||||||
|
Do we need to do rescaling or not.
|
||||||
|
Returns:
|
||||||
|
tensor dropped out"""
|
||||||
|
if sparse_tensor == True:
|
||||||
|
if is_training:
|
||||||
|
with tf.variable_scope("sparse_dropout"):
|
||||||
|
values = input_tensor.values
|
||||||
|
keep_mask = tf.keras.backend.random_binomial(
|
||||||
|
tf.shape(values), p=1 - rate, dtype=tf.float32, seed=None
|
||||||
|
)
|
||||||
|
keep_mask.set_shape([None])
|
||||||
|
keep_mask = tf.cast(keep_mask, tf.bool)
|
||||||
|
|
||||||
|
keep_indices = tf.boolean_mask(input_tensor.indices, keep_mask, axis=0)
|
||||||
|
keep_values = tf.boolean_mask(values, keep_mask, axis=0)
|
||||||
|
|
||||||
|
dropped_tensor = tf.SparseTensor(keep_indices, keep_values, input_tensor.dense_shape)
|
||||||
|
return dropped_tensor
|
||||||
|
else:
|
||||||
|
return input_tensor
|
||||||
|
elif sparse_tensor == False:
|
||||||
|
return tf.layers.dropout(input_tensor, rate=rate, training=is_training)
|
||||||
|
|
||||||
|
|
||||||
|
def adaptive_transformation(bn_normalized_dense, is_training, func_type="default"):
|
||||||
|
assert func_type in [
|
||||||
|
"default",
|
||||||
|
"tiny",
|
||||||
|
], f"fun_type can only be one of default and tiny, but get {func_type}"
|
||||||
|
|
||||||
|
gw_normalized_dense = tf.expand_dims(bn_normalized_dense, -1)
|
||||||
|
group_num = bn_normalized_dense.shape.as_list()[1]
|
||||||
|
|
||||||
|
if func_type == "default":
|
||||||
|
gw_normalized_dense = FastGroupWiseTrans(
|
||||||
|
group_num, 1, 8, name="groupwise_1", activation=tf.tanh, init_multiplier=8
|
||||||
|
)(gw_normalized_dense)
|
||||||
|
|
||||||
|
gw_normalized_dense = FastGroupWiseTrans(
|
||||||
|
group_num, 8, 4, name="groupwise_2", activation=tf.tanh, init_multiplier=8
|
||||||
|
)(gw_normalized_dense)
|
||||||
|
|
||||||
|
gw_normalized_dense = FastGroupWiseTrans(
|
||||||
|
group_num, 4, 1, name="groupwise_3", activation=tf.tanh, init_multiplier=8
|
||||||
|
)(gw_normalized_dense)
|
||||||
|
elif func_type == "tiny":
|
||||||
|
gw_normalized_dense = FastGroupWiseTrans(
|
||||||
|
group_num, 1, 2, name="groupwise_1", activation=tf.tanh, init_multiplier=8
|
||||||
|
)(gw_normalized_dense)
|
||||||
|
|
||||||
|
gw_normalized_dense = FastGroupWiseTrans(
|
||||||
|
group_num, 2, 1, name="groupwise_2", activation=tf.tanh, init_multiplier=8
|
||||||
|
)(gw_normalized_dense)
|
||||||
|
|
||||||
|
gw_normalized_dense = FastGroupWiseTrans(
|
||||||
|
group_num, 1, 1, name="groupwise_3", activation=tf.tanh, init_multiplier=8
|
||||||
|
)(gw_normalized_dense)
|
||||||
|
|
||||||
|
gw_normalized_dense = tf.squeeze(gw_normalized_dense, [-1])
|
||||||
|
bn_gw_normalized_dense = tf.layers.batch_normalization(
|
||||||
|
gw_normalized_dense,
|
||||||
|
training=is_training,
|
||||||
|
renorm_momentum=0.9999,
|
||||||
|
momentum=0.9999,
|
||||||
|
renorm=is_training,
|
||||||
|
trainable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return bn_gw_normalized_dense
|
||||||
|
|
||||||
|
|
||||||
|
class FastGroupWiseTrans(object):
|
||||||
|
"""
|
||||||
|
used to apply group-wise fully connected layers to the input.
|
||||||
|
it applies a tiny, unique MLP to each individual feature."""
|
||||||
|
|
||||||
|
def __init__(self, group_num, input_dim, out_dim, name, activation=None, init_multiplier=1):
|
||||||
|
self.group_num = group_num
|
||||||
|
self.input_dim = input_dim
|
||||||
|
self.out_dim = out_dim
|
||||||
|
self.activation = activation
|
||||||
|
self.init_multiplier = init_multiplier
|
||||||
|
|
||||||
|
self.w = tf.get_variable(
|
||||||
|
name + "_group_weight",
|
||||||
|
[1, group_num, input_dim, out_dim],
|
||||||
|
initializer=customized_glorot_uniform(
|
||||||
|
fan_in=input_dim * init_multiplier, fan_out=out_dim * init_multiplier
|
||||||
|
),
|
||||||
|
trainable=True,
|
||||||
|
)
|
||||||
|
self.b = tf.get_variable(
|
||||||
|
name + "_group_bias",
|
||||||
|
[1, group_num, out_dim],
|
||||||
|
initializer=tf.constant_initializer(0.0),
|
||||||
|
trainable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
def __call__(self, input_tensor):
|
||||||
|
"""
|
||||||
|
input_tensor: batch_size x group_num x input_dim
|
||||||
|
output_tensor: batch_size x group_num x out_dim"""
|
||||||
|
input_tensor_expand = tf.expand_dims(input_tensor, axis=-1)
|
||||||
|
|
||||||
|
output_tensor = tf.add(
|
||||||
|
tf.reduce_sum(tf.multiply(input_tensor_expand, self.w), axis=-2, keepdims=False),
|
||||||
|
self.b,
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.activation is not None:
|
||||||
|
output_tensor = self.activation(output_tensor)
|
||||||
|
return output_tensor
|
||||||
|
|
||||||
|
|
||||||
|
class GroupWiseTrans(object):
|
||||||
|
"""
|
||||||
|
Used to apply group fully connected layers to the input.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, group_num, input_dim, out_dim, name, activation=None):
|
||||||
|
self.group_num = group_num
|
||||||
|
self.input_dim = input_dim
|
||||||
|
self.out_dim = out_dim
|
||||||
|
self.activation = activation
|
||||||
|
|
||||||
|
w_list, b_list = [], []
|
||||||
|
for idx in range(out_dim):
|
||||||
|
this_w = tf.get_variable(
|
||||||
|
name + f"_group_weight_{idx}",
|
||||||
|
[1, group_num, input_dim],
|
||||||
|
initializer=tf.keras.initializers.glorot_uniform(),
|
||||||
|
trainable=True,
|
||||||
|
)
|
||||||
|
this_b = tf.get_variable(
|
||||||
|
name + f"_group_bias_{idx}",
|
||||||
|
[1, group_num, 1],
|
||||||
|
initializer=tf.constant_initializer(0.0),
|
||||||
|
trainable=True,
|
||||||
|
)
|
||||||
|
w_list.append(this_w)
|
||||||
|
b_list.append(this_b)
|
||||||
|
self.w_list = w_list
|
||||||
|
self.b_list = b_list
|
||||||
|
|
||||||
|
def __call__(self, input_tensor):
|
||||||
|
"""
|
||||||
|
input_tensor: batch_size x group_num x input_dim
|
||||||
|
output_tensor: batch_size x group_num x out_dim
|
||||||
|
"""
|
||||||
|
out_tensor_list = []
|
||||||
|
for idx in range(self.out_dim):
|
||||||
|
this_res = (
|
||||||
|
tf.reduce_sum(input_tensor * self.w_list[idx], axis=-1, keepdims=True) + self.b_list[idx]
|
||||||
|
)
|
||||||
|
out_tensor_list.append(this_res)
|
||||||
|
output_tensor = tf.concat(out_tensor_list, axis=-1)
|
||||||
|
|
||||||
|
if self.activation is not None:
|
||||||
|
output_tensor = self.activation(output_tensor)
|
||||||
|
return output_tensor
|
||||||
|
|
||||||
|
|
||||||
|
def add_scalar_summary(var, name, name_scope="hist_dense_feature/"):
|
||||||
|
with tf.name_scope("summaries/"):
|
||||||
|
with tf.name_scope(name_scope):
|
||||||
|
tf.summary.scalar(name, var)
|
||||||
|
|
||||||
|
|
||||||
|
def add_histogram_summary(var, name, name_scope="hist_dense_feature/"):
|
||||||
|
with tf.name_scope("summaries/"):
|
||||||
|
with tf.name_scope(name_scope):
|
||||||
|
tf.summary.histogram(name, tf.reshape(var, [-1]))
|
||||||
|
|
||||||
|
|
||||||
|
def sparse_clip_by_value(sparse_tf, min_val, max_val):
|
||||||
|
new_vals = tf.clip_by_value(sparse_tf.values, min_val, max_val)
|
||||||
|
return tf.SparseTensor(sparse_tf.indices, new_vals, sparse_tf.dense_shape)
|
||||||
|
|
||||||
|
|
||||||
|
def check_numerics_with_msg(tensor, message="", sparse_tensor=False):
|
||||||
|
if sparse_tensor:
|
||||||
|
values = tf.debugging.check_numerics(tensor.values, message=message)
|
||||||
|
return tf.SparseTensor(tensor.indices, values, tensor.dense_shape)
|
||||||
|
else:
|
||||||
|
return tf.debugging.check_numerics(tensor, message=message)
|
||||||
|
|
||||||
|
|
||||||
|
def pad_empty_sparse_tensor(tensor):
|
||||||
|
dummy_tensor = tf.SparseTensor(
|
||||||
|
indices=[[0, 0]],
|
||||||
|
values=[0.00001],
|
||||||
|
dense_shape=tensor.dense_shape,
|
||||||
|
)
|
||||||
|
result = tf.cond(
|
||||||
|
tf.equal(tf.size(tensor.values), 0),
|
||||||
|
lambda: dummy_tensor,
|
||||||
|
lambda: tensor,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def filter_nans_and_infs(tensor, sparse_tensor=False):
|
||||||
|
if sparse_tensor:
|
||||||
|
sparse_values = tensor.values
|
||||||
|
filtered_val = tf.where(
|
||||||
|
tf.logical_or(tf.is_nan(sparse_values), tf.is_inf(sparse_values)),
|
||||||
|
tf.zeros_like(sparse_values),
|
||||||
|
sparse_values,
|
||||||
|
)
|
||||||
|
return tf.SparseTensor(tensor.indices, filtered_val, tensor.dense_shape)
|
||||||
|
else:
|
||||||
|
return tf.where(
|
||||||
|
tf.logical_or(tf.is_nan(tensor), tf.is_inf(tensor)), tf.zeros_like(tensor), tensor
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def generate_disliked_mask(labels):
|
||||||
|
"""Generate a disliked mask where only samples with dislike labels are set to 1 otherwise set to 0.
|
||||||
|
Args:
|
||||||
|
labels: labels of training samples, which is a 2D tensor of shape batch_size x 3: [OONCs, engagements, dislikes]
|
||||||
|
Returns:
|
||||||
|
1D tensor of shape batch_size x 1: [dislikes (booleans)]
|
||||||
|
"""
|
||||||
|
return tf.equal(tf.reshape(labels[:, 2], shape=[-1, 1]), 1)
|
309
pushservice/src/main/python/models/libs/warm_start_utils.py
Normal file
309
pushservice/src/main/python/models/libs/warm_start_utils.py
Normal file
@ -0,0 +1,309 @@
|
|||||||
|
from collections import OrderedDict
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
from os.path import join
|
||||||
|
|
||||||
|
from twitter.magicpony.common import file_access
|
||||||
|
import twml
|
||||||
|
|
||||||
|
from .model_utils import read_config
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from scipy import stats
|
||||||
|
import tensorflow.compat.v1 as tf
|
||||||
|
|
||||||
|
|
||||||
|
# checkstyle: noqa
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_type_to_tensors_to_change_axis():
|
||||||
|
model_type_to_tensors_to_change_axis = {
|
||||||
|
"magic_recs/model/batch_normalization/beta": ([0], "continuous"),
|
||||||
|
"magic_recs/model/batch_normalization/gamma": ([0], "continuous"),
|
||||||
|
"magic_recs/model/batch_normalization/moving_mean": ([0], "continuous"),
|
||||||
|
"magic_recs/model/batch_normalization/moving_stddev": ([0], "continuous"),
|
||||||
|
"magic_recs/model/batch_normalization/moving_variance": ([0], "continuous"),
|
||||||
|
"magic_recs/model/batch_normalization/renorm_mean": ([0], "continuous"),
|
||||||
|
"magic_recs/model/batch_normalization/renorm_stddev": ([0], "continuous"),
|
||||||
|
"magic_recs/model/logits/EngagementGivenOONC_logits/clem_net_1/block2_4/channel_wise_dense_4/kernel": (
|
||||||
|
[1],
|
||||||
|
"all",
|
||||||
|
),
|
||||||
|
"magic_recs/model/logits/OONC_logits/clem_net/block2/channel_wise_dense/kernel": ([1], "all"),
|
||||||
|
}
|
||||||
|
|
||||||
|
return model_type_to_tensors_to_change_axis
|
||||||
|
|
||||||
|
|
||||||
|
def mkdirp(dirname):
|
||||||
|
if not tf.io.gfile.exists(dirname):
|
||||||
|
tf.io.gfile.makedirs(dirname)
|
||||||
|
|
||||||
|
|
||||||
|
def rename_dir(dirname, dst):
|
||||||
|
file_access.hdfs.mv(dirname, dst)
|
||||||
|
|
||||||
|
|
||||||
|
def rmdir(dirname):
|
||||||
|
if tf.io.gfile.exists(dirname):
|
||||||
|
if tf.io.gfile.isdir(dirname):
|
||||||
|
tf.io.gfile.rmtree(dirname)
|
||||||
|
else:
|
||||||
|
tf.io.gfile.remove(dirname)
|
||||||
|
|
||||||
|
|
||||||
|
def get_var_dict(checkpoint_path):
|
||||||
|
checkpoint = tf.train.get_checkpoint_state(checkpoint_path)
|
||||||
|
var_dict = OrderedDict()
|
||||||
|
with tf.Session() as sess:
|
||||||
|
all_var_list = tf.train.list_variables(checkpoint_path)
|
||||||
|
for var_name, _ in all_var_list:
|
||||||
|
# Load the variable
|
||||||
|
var = tf.train.load_variable(checkpoint_path, var_name)
|
||||||
|
var_dict[var_name] = var
|
||||||
|
return var_dict
|
||||||
|
|
||||||
|
|
||||||
|
def get_continunous_mapping_from_feat_list(old_feature_list, new_feature_list):
|
||||||
|
"""
|
||||||
|
get var_ind for old_feature and corresponding var_ind for new_feature
|
||||||
|
"""
|
||||||
|
new_var_ind, old_var_ind = [], []
|
||||||
|
for this_new_id, this_new_name in enumerate(new_feature_list):
|
||||||
|
if this_new_name in old_feature_list:
|
||||||
|
this_old_id = old_feature_list.index(this_new_name)
|
||||||
|
new_var_ind.append(this_new_id)
|
||||||
|
old_var_ind.append(this_old_id)
|
||||||
|
return np.asarray(old_var_ind), np.asarray(new_var_ind)
|
||||||
|
|
||||||
|
|
||||||
|
def get_continuous_mapping_from_feat_dict(old_feature_dict, new_feature_dict):
|
||||||
|
"""
|
||||||
|
get var_ind for old_feature and corresponding var_ind for new_feature
|
||||||
|
"""
|
||||||
|
old_cont = old_feature_dict["continuous"]
|
||||||
|
old_bin = old_feature_dict["binary"]
|
||||||
|
|
||||||
|
new_cont = new_feature_dict["continuous"]
|
||||||
|
new_bin = new_feature_dict["binary"]
|
||||||
|
|
||||||
|
_dummy_sparse_feat = [f"sparse_feature_{_idx}" for _idx in range(100)]
|
||||||
|
|
||||||
|
cont_old_var_ind, cont_new_var_ind = get_continunous_mapping_from_feat_list(old_cont, new_cont)
|
||||||
|
|
||||||
|
all_old_var_ind, all_new_var_ind = get_continunous_mapping_from_feat_list(
|
||||||
|
old_cont + old_bin + _dummy_sparse_feat, new_cont + new_bin + _dummy_sparse_feat
|
||||||
|
)
|
||||||
|
|
||||||
|
_res = {
|
||||||
|
"continuous": (cont_old_var_ind, cont_new_var_ind),
|
||||||
|
"all": (all_old_var_ind, all_new_var_ind),
|
||||||
|
}
|
||||||
|
|
||||||
|
return _res
|
||||||
|
|
||||||
|
|
||||||
|
def warm_start_from_var_dict(
|
||||||
|
old_ckpt_path,
|
||||||
|
var_ind_dict,
|
||||||
|
output_dir,
|
||||||
|
new_len_var,
|
||||||
|
var_to_change_dict_fn=get_model_type_to_tensors_to_change_axis,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Parameters:
|
||||||
|
old_ckpt_path (str): path to the old checkpoint path
|
||||||
|
new_var_ind (array of int): index to overlapping features in new var between old and new feature list.
|
||||||
|
old_var_ind (array of int): index to overlapping features in old var between old and new feature list.
|
||||||
|
|
||||||
|
output_dir (str): dir that used to write modified checkpoint
|
||||||
|
new_len_var ({str:int}): number of feature in the new feature list.
|
||||||
|
var_to_change_dict_fn (dict): A function to get the dictionary of format {var_name: dim_to_change}
|
||||||
|
"""
|
||||||
|
old_var_dict = get_var_dict(old_ckpt_path)
|
||||||
|
|
||||||
|
ckpt_file_name = os.path.basename(old_ckpt_path)
|
||||||
|
mkdirp(output_dir)
|
||||||
|
output_path = join(output_dir, ckpt_file_name)
|
||||||
|
|
||||||
|
tensors_to_change = var_to_change_dict_fn()
|
||||||
|
tf.compat.v1.reset_default_graph()
|
||||||
|
|
||||||
|
with tf.Session() as sess:
|
||||||
|
var_name_shape_list = tf.train.list_variables(old_ckpt_path)
|
||||||
|
count = 0
|
||||||
|
|
||||||
|
for var_name, var_shape in var_name_shape_list:
|
||||||
|
old_var = old_var_dict[var_name]
|
||||||
|
if var_name in tensors_to_change.keys():
|
||||||
|
_info_tuple = tensors_to_change[var_name]
|
||||||
|
dims_to_remove_from, var_type = _info_tuple
|
||||||
|
|
||||||
|
new_var_ind, old_var_ind = var_ind_dict[var_type]
|
||||||
|
|
||||||
|
this_shape = list(old_var.shape)
|
||||||
|
for this_dim in dims_to_remove_from:
|
||||||
|
this_shape[this_dim] = new_len_var[var_type]
|
||||||
|
|
||||||
|
stddev = np.std(old_var)
|
||||||
|
truncated_norm_generator = stats.truncnorm(-0.5, 0.5, loc=0, scale=stddev)
|
||||||
|
size = np.prod(this_shape)
|
||||||
|
new_var = truncated_norm_generator.rvs(size).reshape(this_shape)
|
||||||
|
new_var = new_var.astype(old_var.dtype)
|
||||||
|
|
||||||
|
new_var = copy_feat_based_on_mapping(
|
||||||
|
new_var, old_var, dims_to_remove_from, new_var_ind, old_var_ind
|
||||||
|
)
|
||||||
|
count = count + 1
|
||||||
|
else:
|
||||||
|
new_var = old_var
|
||||||
|
var = tf.Variable(new_var, name=var_name)
|
||||||
|
assert count == len(tensors_to_change.keys()), "not all variables are exchanged.\n"
|
||||||
|
saver = tf.train.Saver()
|
||||||
|
sess.run(tf.global_variables_initializer())
|
||||||
|
saver.save(sess, output_path)
|
||||||
|
return output_path
|
||||||
|
|
||||||
|
|
||||||
|
def copy_feat_based_on_mapping(new_array, old_array, dims_to_remove_from, new_var_ind, old_var_ind):
|
||||||
|
if dims_to_remove_from == [0, 1]:
|
||||||
|
for this_new_ind, this_old_ind in zip(new_var_ind, old_var_ind):
|
||||||
|
new_array[this_new_ind, new_var_ind] = old_array[this_old_ind, old_var_ind]
|
||||||
|
elif dims_to_remove_from == [0]:
|
||||||
|
new_array[new_var_ind] = old_array[old_var_ind]
|
||||||
|
elif dims_to_remove_from == [1]:
|
||||||
|
new_array[:, new_var_ind] = old_array[:, old_var_ind]
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f"undefined dims_to_remove_from pattern: ({dims_to_remove_from})")
|
||||||
|
return new_array
|
||||||
|
|
||||||
|
|
||||||
|
def read_file(filename, decode=False):
|
||||||
|
"""
|
||||||
|
Reads contents from a file and optionally decodes it.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
filename:
|
||||||
|
path to file where the contents will be loaded from.
|
||||||
|
Accepts HDFS and local paths.
|
||||||
|
decode:
|
||||||
|
False or 'json'. When decode='json', contents is decoded
|
||||||
|
with json.loads. When False, contents is returned as is.
|
||||||
|
"""
|
||||||
|
graph = tf.Graph()
|
||||||
|
with graph.as_default():
|
||||||
|
read = tf.read_file(filename)
|
||||||
|
|
||||||
|
with tf.Session(graph=graph) as sess:
|
||||||
|
contents = sess.run(read)
|
||||||
|
if not isinstance(contents, str):
|
||||||
|
contents = contents.decode()
|
||||||
|
|
||||||
|
if decode == "json":
|
||||||
|
contents = json.loads(contents)
|
||||||
|
|
||||||
|
return contents
|
||||||
|
|
||||||
|
|
||||||
|
def read_feat_list_from_disk(file_path):
|
||||||
|
return read_file(file_path, decode="json")
|
||||||
|
|
||||||
|
|
||||||
|
def get_feature_list_for_light_ranking(feature_list_path, data_spec_path):
|
||||||
|
feature_list = read_config(feature_list_path).items()
|
||||||
|
string_feat_list = [f[0] for f in feature_list if f[1] != "S"]
|
||||||
|
|
||||||
|
feature_config_builder = twml.contrib.feature_config.FeatureConfigBuilder(
|
||||||
|
data_spec_path=data_spec_path
|
||||||
|
)
|
||||||
|
feature_config_builder = feature_config_builder.extract_feature_group(
|
||||||
|
feature_regexes=string_feat_list,
|
||||||
|
group_name="continuous",
|
||||||
|
default_value=-1,
|
||||||
|
type_filter=["CONTINUOUS"],
|
||||||
|
)
|
||||||
|
feature_config = feature_config_builder.build()
|
||||||
|
feature_list = feature_config_builder._feature_group_extraction_configs[0].feature_map[
|
||||||
|
"CONTINUOUS"
|
||||||
|
]
|
||||||
|
return feature_list
|
||||||
|
|
||||||
|
|
||||||
|
def get_feature_list_for_heavy_ranking(feature_list_path, data_spec_path):
|
||||||
|
feature_list = read_config(feature_list_path).items()
|
||||||
|
string_feat_list = [f[0] for f in feature_list if f[1] != "S"]
|
||||||
|
|
||||||
|
feature_config_builder = twml.contrib.feature_config.FeatureConfigBuilder(
|
||||||
|
data_spec_path=data_spec_path
|
||||||
|
)
|
||||||
|
feature_config_builder = feature_config_builder.extract_feature_group(
|
||||||
|
feature_regexes=string_feat_list,
|
||||||
|
group_name="continuous",
|
||||||
|
default_value=-1,
|
||||||
|
type_filter=["CONTINUOUS"],
|
||||||
|
)
|
||||||
|
|
||||||
|
feature_config_builder = feature_config_builder.extract_feature_group(
|
||||||
|
feature_regexes=string_feat_list,
|
||||||
|
group_name="binary",
|
||||||
|
default_value=False,
|
||||||
|
type_filter=["BINARY"],
|
||||||
|
)
|
||||||
|
|
||||||
|
feature_config_builder = feature_config_builder.build()
|
||||||
|
|
||||||
|
continuous_feature_list = feature_config_builder._feature_group_extraction_configs[0].feature_map[
|
||||||
|
"CONTINUOUS"
|
||||||
|
]
|
||||||
|
|
||||||
|
binary_feature_list = feature_config_builder._feature_group_extraction_configs[1].feature_map[
|
||||||
|
"BINARY"
|
||||||
|
]
|
||||||
|
return {"continuous": continuous_feature_list, "binary": binary_feature_list}
|
||||||
|
|
||||||
|
|
||||||
|
def warm_start_checkpoint(
|
||||||
|
old_best_ckpt_folder,
|
||||||
|
old_feature_list_path,
|
||||||
|
feature_allow_list_path,
|
||||||
|
data_spec_path,
|
||||||
|
output_ckpt_folder,
|
||||||
|
*args,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Reads old checkpoint and the old feature list, and create a new ckpt warm started from old ckpt using new features .
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
old_best_ckpt_folder:
|
||||||
|
path to the best_checkpoint_folder for old model
|
||||||
|
old_feature_list_path:
|
||||||
|
path to the json file that stores the list of continuous features used in old models.
|
||||||
|
feature_allow_list_path:
|
||||||
|
yaml file that contain the feature allow list.
|
||||||
|
data_spec_path:
|
||||||
|
path to the data_spec file
|
||||||
|
output_ckpt_folder:
|
||||||
|
folder that contains the modified ckpt.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
path to the modified ckpt."""
|
||||||
|
old_ckpt_path = tf.train.latest_checkpoint(old_best_ckpt_folder, latest_filename=None)
|
||||||
|
|
||||||
|
new_feature_dict = get_feature_list(feature_allow_list_path, data_spec_path)
|
||||||
|
old_feature_dict = read_feat_list_from_disk(old_feature_list_path)
|
||||||
|
|
||||||
|
var_ind_dict = get_continuous_mapping_from_feat_dict(new_feature_dict, old_feature_dict)
|
||||||
|
|
||||||
|
new_len_var = {
|
||||||
|
"continuous": len(new_feature_dict["continuous"]),
|
||||||
|
"all": len(new_feature_dict["continuous"] + new_feature_dict["binary"]) + 100,
|
||||||
|
}
|
||||||
|
|
||||||
|
warm_started_ckpt_path = warm_start_from_var_dict(
|
||||||
|
old_ckpt_path,
|
||||||
|
var_ind_dict,
|
||||||
|
output_dir=output_ckpt_folder,
|
||||||
|
new_len_var=new_len_var,
|
||||||
|
)
|
||||||
|
|
||||||
|
return warm_started_ckpt_path
|
69
pushservice/src/main/python/models/light_ranking/BUILD
Normal file
69
pushservice/src/main/python/models/light_ranking/BUILD
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
#":mlwf_libs",
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "eval_model",
|
||||||
|
source = "eval_model.py",
|
||||||
|
dependencies = [
|
||||||
|
":libs",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/light_ranking:eval_model",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "train_model",
|
||||||
|
source = "deep_norm.py",
|
||||||
|
dependencies = [
|
||||||
|
":libs",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/light_ranking:train_model",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "train_model_local",
|
||||||
|
source = "deep_norm.py",
|
||||||
|
dependencies = [
|
||||||
|
":libs",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/light_ranking:train_model_local",
|
||||||
|
"twml",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "eval_model_local",
|
||||||
|
source = "eval_model.py",
|
||||||
|
dependencies = [
|
||||||
|
":libs",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/light_ranking:eval_model_local",
|
||||||
|
"twml",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python37_binary(
|
||||||
|
name = "mlwf_model",
|
||||||
|
source = "deep_norm.py",
|
||||||
|
dependencies = [
|
||||||
|
":mlwf_libs",
|
||||||
|
"3rdparty/python/_closures/frigate/frigate-pushservice-opensource/src/main/python/models/light_ranking:mlwf_model",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "libs",
|
||||||
|
sources = ["**/*.py"],
|
||||||
|
tags = ["no-mypy"],
|
||||||
|
dependencies = [
|
||||||
|
"src/python/twitter/deepbird/projects/magic_recs/libs",
|
||||||
|
"src/python/twitter/deepbird/util/data",
|
||||||
|
"twml:twml-nodeps",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
python3_library(
|
||||||
|
name = "mlwf_libs",
|
||||||
|
sources = ["**/*.py"],
|
||||||
|
tags = ["no-mypy"],
|
||||||
|
dependencies = [
|
||||||
|
"src/python/twitter/deepbird/projects/magic_recs/libs",
|
||||||
|
"twml",
|
||||||
|
],
|
||||||
|
)
|
14
pushservice/src/main/python/models/light_ranking/README.md
Normal file
14
pushservice/src/main/python/models/light_ranking/README.md
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Notification Light Ranker Model
|
||||||
|
|
||||||
|
## Model Context
|
||||||
|
There are 4 major components of Twitter notifications recommendation system: 1) candidate generation 2) light ranking 3) heavy ranking & 4) quality control. This notification light ranker model bridges candidate generation and heavy ranking by pre-selecting highly-relevant candidates from the initial huge candidate pool. It’s a light-weight model to reduce system cost during heavy ranking without hurting user experience.
|
||||||
|
|
||||||
|
## Directory Structure
|
||||||
|
- BUILD: this file defines python library dependencies
|
||||||
|
- model_pools_mlp.py: this file defines tensorflow model architecture for the notification light ranker model
|
||||||
|
- deep_norm.py: this file contains 1) how to build the tensorflow graph with specified model architecture, loss function and training configuration. 2) how to set up the overall model training & evaluation pipeline
|
||||||
|
- eval_model.py: the main python entry file to set up the overall model evaluation pipeline
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
226
pushservice/src/main/python/models/light_ranking/deep_norm.py
Normal file
226
pushservice/src/main/python/models/light_ranking/deep_norm.py
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from functools import partial
|
||||||
|
import os
|
||||||
|
|
||||||
|
from twitter.cortex.ml.embeddings.common.helpers import decode_str_or_unicode
|
||||||
|
import twml
|
||||||
|
from twml.trainers import DataRecordTrainer
|
||||||
|
|
||||||
|
from ..libs.get_feat_config import get_feature_config_light_ranking, LABELS_LR
|
||||||
|
from ..libs.graph_utils import get_trainable_variables
|
||||||
|
from ..libs.group_metrics import (
|
||||||
|
run_group_metrics_light_ranking,
|
||||||
|
run_group_metrics_light_ranking_in_bq,
|
||||||
|
)
|
||||||
|
from ..libs.metric_fn_utils import get_metric_fn
|
||||||
|
from ..libs.model_args import get_arg_parser_light_ranking
|
||||||
|
from ..libs.model_utils import read_config
|
||||||
|
from ..libs.warm_start_utils import get_feature_list_for_light_ranking
|
||||||
|
from .model_pools_mlp import light_ranking_mlp_ngbdt
|
||||||
|
|
||||||
|
import tensorflow.compat.v1 as tf
|
||||||
|
from tensorflow.compat.v1 import logging
|
||||||
|
|
||||||
|
|
||||||
|
# checkstyle: noqa
|
||||||
|
|
||||||
|
|
||||||
|
def build_graph(
|
||||||
|
features, label, mode, params, config=None, run_light_ranking_group_metrics_in_bq=False
|
||||||
|
):
|
||||||
|
is_training = mode == tf.estimator.ModeKeys.TRAIN
|
||||||
|
this_model_func = light_ranking_mlp_ngbdt
|
||||||
|
model_output = this_model_func(features, is_training, params, label)
|
||||||
|
|
||||||
|
logits = model_output["output"]
|
||||||
|
graph_output = {}
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# define graph output dict
|
||||||
|
# --------------------------------------------------------
|
||||||
|
if mode == tf.estimator.ModeKeys.PREDICT:
|
||||||
|
loss = None
|
||||||
|
output_label = "prediction"
|
||||||
|
if params.task_name in LABELS_LR:
|
||||||
|
output = tf.nn.sigmoid(logits)
|
||||||
|
output = tf.clip_by_value(output, 0, 1)
|
||||||
|
|
||||||
|
if run_light_ranking_group_metrics_in_bq:
|
||||||
|
graph_output["trace_id"] = features["meta.trace_id"]
|
||||||
|
graph_output["target"] = features["meta.ranking.weighted_oonc_model_score"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid Task Name !")
|
||||||
|
|
||||||
|
else:
|
||||||
|
output_label = "output"
|
||||||
|
weights = tf.cast(features["weights"], dtype=tf.float32, name="RecordWeights")
|
||||||
|
|
||||||
|
if params.task_name in LABELS_LR:
|
||||||
|
if params.use_record_weight:
|
||||||
|
weights = tf.clip_by_value(
|
||||||
|
1.0 / (1.0 + weights + params.smooth_weight), params.min_record_weight, 1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
loss = tf.reduce_sum(
|
||||||
|
tf.nn.sigmoid_cross_entropy_with_logits(labels=label, logits=logits) * weights
|
||||||
|
) / (tf.reduce_sum(weights))
|
||||||
|
else:
|
||||||
|
loss = tf.reduce_mean(tf.nn.sigmoid_cross_entropy_with_logits(labels=label, logits=logits))
|
||||||
|
output = tf.nn.sigmoid(logits)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid Task Name !")
|
||||||
|
|
||||||
|
train_op = None
|
||||||
|
if mode == tf.estimator.ModeKeys.TRAIN:
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# get train_op
|
||||||
|
# --------------------------------------------------------
|
||||||
|
optimizer = tf.train.GradientDescentOptimizer(learning_rate=params.learning_rate)
|
||||||
|
update_ops = set(tf.get_collection(tf.GraphKeys.UPDATE_OPS))
|
||||||
|
variables = get_trainable_variables(
|
||||||
|
all_trainable_variables=tf.trainable_variables(), trainable_regexes=params.trainable_regexes
|
||||||
|
)
|
||||||
|
with tf.control_dependencies(update_ops):
|
||||||
|
train_op = twml.optimizers.optimize_loss(
|
||||||
|
loss=loss,
|
||||||
|
variables=variables,
|
||||||
|
global_step=tf.train.get_global_step(),
|
||||||
|
optimizer=optimizer,
|
||||||
|
learning_rate=params.learning_rate,
|
||||||
|
learning_rate_decay_fn=twml.learning_rate_decay.get_learning_rate_decay_fn(params),
|
||||||
|
)
|
||||||
|
|
||||||
|
graph_output[output_label] = output
|
||||||
|
graph_output["loss"] = loss
|
||||||
|
graph_output["train_op"] = train_op
|
||||||
|
return graph_output
|
||||||
|
|
||||||
|
|
||||||
|
def get_params(args=None):
|
||||||
|
parser = get_arg_parser_light_ranking()
|
||||||
|
if args is None:
|
||||||
|
return parser.parse_args()
|
||||||
|
else:
|
||||||
|
return parser.parse_args(args)
|
||||||
|
|
||||||
|
|
||||||
|
def _main():
|
||||||
|
opt = get_params()
|
||||||
|
logging.info("parse is: ")
|
||||||
|
logging.info(opt)
|
||||||
|
|
||||||
|
feature_list = read_config(opt.feature_list).items()
|
||||||
|
feature_config = get_feature_config_light_ranking(
|
||||||
|
data_spec_path=opt.data_spec,
|
||||||
|
feature_list_provided=feature_list,
|
||||||
|
opt=opt,
|
||||||
|
add_gbdt=opt.use_gbdt_features,
|
||||||
|
run_light_ranking_group_metrics_in_bq=opt.run_light_ranking_group_metrics_in_bq,
|
||||||
|
)
|
||||||
|
feature_list_path = opt.feature_list
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Create Trainer
|
||||||
|
# --------------------------------------------------------
|
||||||
|
trainer = DataRecordTrainer(
|
||||||
|
name=opt.model_trainer_name,
|
||||||
|
params=opt,
|
||||||
|
build_graph_fn=build_graph,
|
||||||
|
save_dir=opt.save_dir,
|
||||||
|
run_config=None,
|
||||||
|
feature_config=feature_config,
|
||||||
|
metric_fn=get_metric_fn(opt.task_name, use_stratify_metrics=False),
|
||||||
|
)
|
||||||
|
if opt.directly_export_best:
|
||||||
|
logging.info("Directly exporting the model without training")
|
||||||
|
else:
|
||||||
|
# ----------------------------------------------------
|
||||||
|
# Model Training & Evaluation
|
||||||
|
# ----------------------------------------------------
|
||||||
|
eval_input_fn = trainer.get_eval_input_fn(repeat=False, shuffle=False)
|
||||||
|
train_input_fn = trainer.get_train_input_fn(shuffle=True)
|
||||||
|
|
||||||
|
if opt.distributed or opt.num_workers is not None:
|
||||||
|
learn = trainer.train_and_evaluate
|
||||||
|
else:
|
||||||
|
learn = trainer.learn
|
||||||
|
logging.info("Training...")
|
||||||
|
start = datetime.now()
|
||||||
|
|
||||||
|
early_stop_metric = "rce_unweighted_" + opt.task_name
|
||||||
|
learn(
|
||||||
|
early_stop_minimize=False,
|
||||||
|
early_stop_metric=early_stop_metric,
|
||||||
|
early_stop_patience=opt.early_stop_patience,
|
||||||
|
early_stop_tolerance=opt.early_stop_tolerance,
|
||||||
|
eval_input_fn=eval_input_fn,
|
||||||
|
train_input_fn=train_input_fn,
|
||||||
|
)
|
||||||
|
|
||||||
|
end = datetime.now()
|
||||||
|
logging.info("Training time: " + str(end - start))
|
||||||
|
|
||||||
|
logging.info("Exporting the models...")
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Do the model exporting
|
||||||
|
# --------------------------------------------------------
|
||||||
|
start = datetime.now()
|
||||||
|
if not opt.export_dir:
|
||||||
|
opt.export_dir = os.path.join(opt.save_dir, "exported_models")
|
||||||
|
|
||||||
|
raw_model_path = twml.contrib.export.export_fn.export_all_models(
|
||||||
|
trainer=trainer,
|
||||||
|
export_dir=opt.export_dir,
|
||||||
|
parse_fn=feature_config.get_parse_fn(),
|
||||||
|
serving_input_receiver_fn=feature_config.get_serving_input_receiver_fn(),
|
||||||
|
export_output_fn=twml.export_output_fns.batch_prediction_continuous_output_fn,
|
||||||
|
)
|
||||||
|
export_model_dir = decode_str_or_unicode(raw_model_path)
|
||||||
|
|
||||||
|
logging.info("Model export time: " + str(datetime.now() - start))
|
||||||
|
logging.info("The saved model directory is: " + opt.save_dir)
|
||||||
|
|
||||||
|
tf.logging.info("getting default continuous_feature_list")
|
||||||
|
continuous_feature_list = get_feature_list_for_light_ranking(feature_list_path, opt.data_spec)
|
||||||
|
continous_feature_list_save_path = os.path.join(opt.save_dir, "continuous_feature_list.json")
|
||||||
|
twml.util.write_file(continous_feature_list_save_path, continuous_feature_list, encode="json")
|
||||||
|
tf.logging.info(f"Finish writting files to {continous_feature_list_save_path}")
|
||||||
|
|
||||||
|
if opt.run_light_ranking_group_metrics:
|
||||||
|
# --------------------------------------------
|
||||||
|
# Run Light Ranking Group Metrics
|
||||||
|
# --------------------------------------------
|
||||||
|
run_group_metrics_light_ranking(
|
||||||
|
trainer=trainer,
|
||||||
|
data_dir=os.path.join(opt.eval_data_dir, opt.eval_start_datetime),
|
||||||
|
model_path=export_model_dir,
|
||||||
|
parse_fn=feature_config.get_parse_fn(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if opt.run_light_ranking_group_metrics_in_bq:
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
# Get Light/Heavy Ranker Predictions for Light Ranking Group Metrics in BigQuery
|
||||||
|
# ----------------------------------------------------------------------------------------
|
||||||
|
trainer_pred = DataRecordTrainer(
|
||||||
|
name=opt.model_trainer_name,
|
||||||
|
params=opt,
|
||||||
|
build_graph_fn=partial(build_graph, run_light_ranking_group_metrics_in_bq=True),
|
||||||
|
save_dir=opt.save_dir + "/tmp/",
|
||||||
|
run_config=None,
|
||||||
|
feature_config=feature_config,
|
||||||
|
metric_fn=get_metric_fn(opt.task_name, use_stratify_metrics=False),
|
||||||
|
)
|
||||||
|
checkpoint_folder = os.path.join(opt.save_dir, "best_checkpoint")
|
||||||
|
checkpoint = tf.train.latest_checkpoint(checkpoint_folder, latest_filename=None)
|
||||||
|
tf.logging.info("\n\nPrediction from Checkpoint: {:}.\n\n".format(checkpoint))
|
||||||
|
run_group_metrics_light_ranking_in_bq(
|
||||||
|
trainer=trainer_pred, params=opt, checkpoint_path=checkpoint
|
||||||
|
)
|
||||||
|
|
||||||
|
tf.logging.info("Done Training & Prediction.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
_main()
|
@ -0,0 +1,89 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from functools import partial
|
||||||
|
import os
|
||||||
|
|
||||||
|
from ..libs.group_metrics import (
|
||||||
|
run_group_metrics_light_ranking,
|
||||||
|
run_group_metrics_light_ranking_in_bq,
|
||||||
|
)
|
||||||
|
from ..libs.metric_fn_utils import get_metric_fn
|
||||||
|
from ..libs.model_args import get_arg_parser_light_ranking
|
||||||
|
from ..libs.model_utils import read_config
|
||||||
|
from .deep_norm import build_graph, DataRecordTrainer, get_config_func, logging
|
||||||
|
|
||||||
|
|
||||||
|
# checkstyle: noqa
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
parser = get_arg_parser_light_ranking()
|
||||||
|
parser.add_argument(
|
||||||
|
"--eval_checkpoint",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="Which checkpoint to use for evaluation",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--saved_model_path",
|
||||||
|
default=None,
|
||||||
|
type=str,
|
||||||
|
help="Path to saved model for evaluation",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--run_binary_metrics",
|
||||||
|
default=False,
|
||||||
|
action="store_true",
|
||||||
|
help="Whether to compute the basic binary metrics for Light Ranking.",
|
||||||
|
)
|
||||||
|
|
||||||
|
opt = parser.parse_args()
|
||||||
|
logging.info("parse is: ")
|
||||||
|
logging.info(opt)
|
||||||
|
|
||||||
|
feature_list = read_config(opt.feature_list).items()
|
||||||
|
feature_config = get_config_func(opt.feat_config_type)(
|
||||||
|
data_spec_path=opt.data_spec,
|
||||||
|
feature_list_provided=feature_list,
|
||||||
|
opt=opt,
|
||||||
|
add_gbdt=opt.use_gbdt_features,
|
||||||
|
run_light_ranking_group_metrics_in_bq=opt.run_light_ranking_group_metrics_in_bq,
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------
|
||||||
|
# Create Trainer
|
||||||
|
# -----------------------------------------------
|
||||||
|
trainer = DataRecordTrainer(
|
||||||
|
name=opt.model_trainer_name,
|
||||||
|
params=opt,
|
||||||
|
build_graph_fn=partial(build_graph, run_light_ranking_group_metrics_in_bq=True),
|
||||||
|
save_dir=opt.save_dir,
|
||||||
|
run_config=None,
|
||||||
|
feature_config=feature_config,
|
||||||
|
metric_fn=get_metric_fn(opt.task_name, use_stratify_metrics=False),
|
||||||
|
)
|
||||||
|
|
||||||
|
# -----------------------------------------------
|
||||||
|
# Model Evaluation
|
||||||
|
# -----------------------------------------------
|
||||||
|
logging.info("Evaluating...")
|
||||||
|
start = datetime.now()
|
||||||
|
|
||||||
|
if opt.run_binary_metrics:
|
||||||
|
eval_input_fn = trainer.get_eval_input_fn(repeat=False, shuffle=False)
|
||||||
|
eval_steps = None if (opt.eval_steps is not None and opt.eval_steps < 0) else opt.eval_steps
|
||||||
|
trainer.estimator.evaluate(eval_input_fn, steps=eval_steps, checkpoint_path=opt.eval_checkpoint)
|
||||||
|
|
||||||
|
if opt.run_light_ranking_group_metrics_in_bq:
|
||||||
|
run_group_metrics_light_ranking_in_bq(
|
||||||
|
trainer=trainer, params=opt, checkpoint_path=opt.eval_checkpoint
|
||||||
|
)
|
||||||
|
|
||||||
|
if opt.run_light_ranking_group_metrics:
|
||||||
|
run_group_metrics_light_ranking(
|
||||||
|
trainer=trainer,
|
||||||
|
data_dir=os.path.join(opt.eval_data_dir, opt.eval_start_datetime),
|
||||||
|
model_path=opt.saved_model_path,
|
||||||
|
parse_fn=feature_config.get_parse_fn(),
|
||||||
|
)
|
||||||
|
|
||||||
|
end = datetime.now()
|
||||||
|
logging.info("Evaluating time: " + str(end - start))
|
@ -0,0 +1,187 @@
|
|||||||
|
import warnings
|
||||||
|
|
||||||
|
from twml.contrib.layers import ZscoreNormalization
|
||||||
|
|
||||||
|
from ...libs.customized_full_sparse import FullSparse
|
||||||
|
from ...libs.get_feat_config import FEAT_CONFIG_DEFAULT_VAL as MISSING_VALUE_MARKER
|
||||||
|
from ...libs.model_utils import (
|
||||||
|
_sparse_feature_fixup,
|
||||||
|
adaptive_transformation,
|
||||||
|
filter_nans_and_infs,
|
||||||
|
get_dense_out,
|
||||||
|
tensor_dropout,
|
||||||
|
)
|
||||||
|
|
||||||
|
import tensorflow.compat.v1 as tf
|
||||||
|
# checkstyle: noqa
|
||||||
|
|
||||||
|
def light_ranking_mlp_ngbdt(features, is_training, params, label=None):
|
||||||
|
return deepnorm_light_ranking(
|
||||||
|
features,
|
||||||
|
is_training,
|
||||||
|
params,
|
||||||
|
label=label,
|
||||||
|
decay=params.momentum,
|
||||||
|
dense_emb_size=params.dense_embedding_size,
|
||||||
|
base_activation=tf.keras.layers.LeakyReLU(),
|
||||||
|
input_dropout_rate=params.dropout,
|
||||||
|
use_gbdt=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def deepnorm_light_ranking(
|
||||||
|
features,
|
||||||
|
is_training,
|
||||||
|
params,
|
||||||
|
label=None,
|
||||||
|
decay=0.99999,
|
||||||
|
dense_emb_size=128,
|
||||||
|
base_activation=None,
|
||||||
|
input_dropout_rate=None,
|
||||||
|
input_dense_type="self_atten_dense",
|
||||||
|
emb_dense_type="self_atten_dense",
|
||||||
|
mlp_dense_type="self_atten_dense",
|
||||||
|
use_gbdt=False,
|
||||||
|
):
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Initial Parameter Checking
|
||||||
|
# --------------------------------------------------------
|
||||||
|
if base_activation is None:
|
||||||
|
base_activation = tf.keras.layers.LeakyReLU()
|
||||||
|
|
||||||
|
if label is not None:
|
||||||
|
warnings.warn(
|
||||||
|
"Label is unused in deepnorm_gbdt. Stop using this argument.",
|
||||||
|
DeprecationWarning,
|
||||||
|
)
|
||||||
|
|
||||||
|
with tf.variable_scope("helper_layers"):
|
||||||
|
full_sparse_layer = FullSparse(
|
||||||
|
output_size=params.sparse_embedding_size,
|
||||||
|
activation=base_activation,
|
||||||
|
use_sparse_grads=is_training,
|
||||||
|
use_binary_values=False,
|
||||||
|
dtype=tf.float32,
|
||||||
|
)
|
||||||
|
input_normalizing_layer = ZscoreNormalization(decay=decay, name="input_normalizing_layer")
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# Feature Selection & Embedding
|
||||||
|
# --------------------------------------------------------
|
||||||
|
if use_gbdt:
|
||||||
|
sparse_gbdt_features = _sparse_feature_fixup(features["gbdt_sparse"], params.input_size_bits)
|
||||||
|
if input_dropout_rate is not None:
|
||||||
|
sparse_gbdt_features = tensor_dropout(
|
||||||
|
sparse_gbdt_features, input_dropout_rate, is_training, sparse_tensor=True
|
||||||
|
)
|
||||||
|
|
||||||
|
total_embed = full_sparse_layer(sparse_gbdt_features, use_binary_values=True)
|
||||||
|
|
||||||
|
if (input_dropout_rate is not None) and is_training:
|
||||||
|
total_embed = total_embed / (1 - input_dropout_rate)
|
||||||
|
|
||||||
|
else:
|
||||||
|
with tf.variable_scope("dense_branch"):
|
||||||
|
dense_continuous_features = filter_nans_and_infs(features["continuous"])
|
||||||
|
|
||||||
|
if params.use_missing_sub_branch:
|
||||||
|
is_missing = tf.equal(dense_continuous_features, MISSING_VALUE_MARKER)
|
||||||
|
continuous_features_filled = tf.where(
|
||||||
|
is_missing,
|
||||||
|
tf.zeros_like(dense_continuous_features),
|
||||||
|
dense_continuous_features,
|
||||||
|
)
|
||||||
|
normalized_features = input_normalizing_layer(
|
||||||
|
continuous_features_filled, is_training, tf.math.logical_not(is_missing)
|
||||||
|
)
|
||||||
|
|
||||||
|
with tf.variable_scope("missing_sub_branch"):
|
||||||
|
missing_feature_embed = get_dense_out(
|
||||||
|
tf.cast(is_missing, tf.float32),
|
||||||
|
dense_emb_size,
|
||||||
|
activation=base_activation,
|
||||||
|
dense_type=input_dense_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
continuous_features_filled = dense_continuous_features
|
||||||
|
normalized_features = input_normalizing_layer(continuous_features_filled, is_training)
|
||||||
|
|
||||||
|
with tf.variable_scope("continuous_sub_branch"):
|
||||||
|
normalized_features = adaptive_transformation(
|
||||||
|
normalized_features, is_training, func_type="tiny"
|
||||||
|
)
|
||||||
|
|
||||||
|
if input_dropout_rate is not None:
|
||||||
|
normalized_features = tensor_dropout(
|
||||||
|
normalized_features,
|
||||||
|
input_dropout_rate,
|
||||||
|
is_training,
|
||||||
|
sparse_tensor=False,
|
||||||
|
)
|
||||||
|
filled_feature_embed = get_dense_out(
|
||||||
|
normalized_features,
|
||||||
|
dense_emb_size,
|
||||||
|
activation=base_activation,
|
||||||
|
dense_type=input_dense_type,
|
||||||
|
)
|
||||||
|
|
||||||
|
if params.use_missing_sub_branch:
|
||||||
|
dense_embed = tf.concat(
|
||||||
|
[filled_feature_embed, missing_feature_embed], axis=1, name="merge_dense_emb"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
dense_embed = filled_feature_embed
|
||||||
|
|
||||||
|
with tf.variable_scope("sparse_branch"):
|
||||||
|
sparse_discrete_features = _sparse_feature_fixup(
|
||||||
|
features["sparse_no_continuous"], params.input_size_bits
|
||||||
|
)
|
||||||
|
if input_dropout_rate is not None:
|
||||||
|
sparse_discrete_features = tensor_dropout(
|
||||||
|
sparse_discrete_features, input_dropout_rate, is_training, sparse_tensor=True
|
||||||
|
)
|
||||||
|
|
||||||
|
discrete_features_embed = full_sparse_layer(sparse_discrete_features, use_binary_values=True)
|
||||||
|
|
||||||
|
if (input_dropout_rate is not None) and is_training:
|
||||||
|
discrete_features_embed = discrete_features_embed / (1 - input_dropout_rate)
|
||||||
|
|
||||||
|
total_embed = tf.concat(
|
||||||
|
[dense_embed, discrete_features_embed],
|
||||||
|
axis=1,
|
||||||
|
name="total_embed",
|
||||||
|
)
|
||||||
|
|
||||||
|
total_embed = tf.layers.batch_normalization(
|
||||||
|
total_embed,
|
||||||
|
training=is_training,
|
||||||
|
renorm_momentum=decay,
|
||||||
|
momentum=decay,
|
||||||
|
renorm=is_training,
|
||||||
|
trainable=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --------------------------------------------------------
|
||||||
|
# MLP Layers
|
||||||
|
# --------------------------------------------------------
|
||||||
|
with tf.variable_scope("MLP_branch"):
|
||||||
|
|
||||||
|
assert params.num_mlp_layers >= 0
|
||||||
|
embed_list = [total_embed] + [None for _ in range(params.num_mlp_layers)]
|
||||||
|
dense_types = [emb_dense_type] + [mlp_dense_type for _ in range(params.num_mlp_layers - 1)]
|
||||||
|
|
||||||
|
for xl in range(1, params.num_mlp_layers + 1):
|
||||||
|
neurons = params.mlp_neuron_scale ** (params.num_mlp_layers + 1 - xl)
|
||||||
|
embed_list[xl] = get_dense_out(
|
||||||
|
embed_list[xl - 1], neurons, activation=base_activation, dense_type=dense_types[xl - 1]
|
||||||
|
)
|
||||||
|
|
||||||
|
if params.task_name in ["Sent", "HeavyRankPosition", "HeavyRankProbability"]:
|
||||||
|
logits = get_dense_out(embed_list[-1], 1, activation=None, dense_type=mlp_dense_type)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError("Invalid Task Name !")
|
||||||
|
|
||||||
|
output_dict = {"output": logits}
|
||||||
|
return output_dict
|
@ -0,0 +1,337 @@
|
|||||||
|
scala_library(
|
||||||
|
sources = ["**/*.scala"],
|
||||||
|
compiler_option_sets = ["fatal_warnings"],
|
||||||
|
strict_deps = True,
|
||||||
|
tags = [
|
||||||
|
"bazel-compatible",
|
||||||
|
],
|
||||||
|
dependencies = [
|
||||||
|
"3rdparty/jvm/com/twitter/bijection:scrooge",
|
||||||
|
"3rdparty/jvm/com/twitter/storehaus:core",
|
||||||
|
"abdecider",
|
||||||
|
"abuse/detection/src/main/thrift/com/twitter/abuse/detection/scoring:thrift-scala",
|
||||||
|
"ann/src/main/scala/com/twitter/ann/common",
|
||||||
|
"ann/src/main/thrift/com/twitter/ann/common:ann-common-scala",
|
||||||
|
"audience-rewards/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"communities/thrift/src/main/thrift/com/twitter/communities:thrift-scala",
|
||||||
|
"configapi/configapi-core",
|
||||||
|
"configapi/configapi-decider",
|
||||||
|
"content-mixer/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"content-recommender/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"copyselectionservice/server/src/main/scala/com/twitter/copyselectionservice/algorithms",
|
||||||
|
"copyselectionservice/thrift/src/main/thrift:copyselectionservice-scala",
|
||||||
|
"cortex-deepbird/thrift/src/main/thrift:thrift-java",
|
||||||
|
"cr-mixer/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"cuad/projects/hashspace/thrift:thrift-scala",
|
||||||
|
"cuad/projects/tagspace/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"detopic/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"discovery-common/src/main/scala/com/twitter/discovery/common/configapi",
|
||||||
|
"discovery-common/src/main/scala/com/twitter/discovery/common/ddg",
|
||||||
|
"discovery-common/src/main/scala/com/twitter/discovery/common/environment",
|
||||||
|
"discovery-common/src/main/scala/com/twitter/discovery/common/fatigue",
|
||||||
|
"discovery-common/src/main/scala/com/twitter/discovery/common/nackwarmupfilter",
|
||||||
|
"discovery-common/src/main/scala/com/twitter/discovery/common/server",
|
||||||
|
"discovery-ds/src/main/thrift/com/twitter/dds/scio/searcher_aggregate_history_srp:searcher_aggregate_history_srp-scala",
|
||||||
|
"escherbird/src/scala/com/twitter/escherbird/util/metadatastitch",
|
||||||
|
"escherbird/src/scala/com/twitter/escherbird/util/uttclient",
|
||||||
|
"escherbird/src/thrift/com/twitter/escherbird/utt:strato-columns-scala",
|
||||||
|
"eventbus/client",
|
||||||
|
"eventdetection/event_context/src/main/scala/com/twitter/eventdetection/event_context/util",
|
||||||
|
"events-recos/events-recos-service/src/main/thrift:events-recos-thrift-scala",
|
||||||
|
"explore/explore-ranker/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"featureswitches/featureswitches-core/src/main/scala",
|
||||||
|
"featureswitches/featureswitches-core/src/main/scala:dynmap",
|
||||||
|
"featureswitches/featureswitches-core/src/main/scala:recipient",
|
||||||
|
"featureswitches/featureswitches-core/src/main/scala:useragent",
|
||||||
|
"featureswitches/featureswitches-core/src/main/scala/com/twitter/featureswitches/v2/builder",
|
||||||
|
"finagle-internal/mtls/src/main/scala/com/twitter/finagle/mtls/authentication",
|
||||||
|
"finagle-internal/mtls/src/main/scala/com/twitter/finagle/mtls/server",
|
||||||
|
"finagle-internal/ostrich-stats",
|
||||||
|
"finagle/finagle-core/src/main",
|
||||||
|
"finagle/finagle-http/src/main/scala",
|
||||||
|
"finagle/finagle-memcached/src/main/scala",
|
||||||
|
"finagle/finagle-stats",
|
||||||
|
"finagle/finagle-thriftmux",
|
||||||
|
"finagle/finagle-tunable/src/main/scala",
|
||||||
|
"finagle/finagle-zipkin-scribe",
|
||||||
|
"finatra-internal/abdecider",
|
||||||
|
"finatra-internal/decider",
|
||||||
|
"finatra-internal/mtls-http/src/main/scala",
|
||||||
|
"finatra-internal/mtls-thriftmux/src/main/scala",
|
||||||
|
"finatra/http-client/src/main/scala",
|
||||||
|
"finatra/http-core/src/main/java/com/twitter/finatra/http",
|
||||||
|
"finatra/http-core/src/main/scala/com/twitter/finatra/http/response",
|
||||||
|
"finatra/http-server/src/main/scala/com/twitter/finatra/http",
|
||||||
|
"finatra/http-server/src/main/scala/com/twitter/finatra/http/filters",
|
||||||
|
"finatra/inject/inject-app/src/main/java/com/twitter/inject/annotations",
|
||||||
|
"finatra/inject/inject-app/src/main/scala",
|
||||||
|
"finatra/inject/inject-core/src/main/scala",
|
||||||
|
"finatra/inject/inject-server/src/main/scala",
|
||||||
|
"finatra/inject/inject-slf4j/src/main/scala/com/twitter/inject",
|
||||||
|
"finatra/inject/inject-thrift-client/src/main/scala",
|
||||||
|
"finatra/inject/inject-utils/src/main/scala",
|
||||||
|
"finatra/utils/src/main/java/com/twitter/finatra/annotations",
|
||||||
|
"fleets/fleets-proxy/thrift/src/main/thrift:fleet-scala",
|
||||||
|
"fleets/fleets-proxy/thrift/src/main/thrift/service:baseservice-scala",
|
||||||
|
"flock-client/src/main/scala",
|
||||||
|
"flock-client/src/main/thrift:thrift-scala",
|
||||||
|
"follow-recommendations-service/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"frigate/frigate-common:base",
|
||||||
|
"frigate/frigate-common:config",
|
||||||
|
"frigate/frigate-common:debug",
|
||||||
|
"frigate/frigate-common:entity_graph_client",
|
||||||
|
"frigate/frigate-common:history",
|
||||||
|
"frigate/frigate-common:logger",
|
||||||
|
"frigate/frigate-common:ml-base",
|
||||||
|
"frigate/frigate-common:ml-feature",
|
||||||
|
"frigate/frigate-common:ml-prediction",
|
||||||
|
"frigate/frigate-common:ntab",
|
||||||
|
"frigate/frigate-common:predicate",
|
||||||
|
"frigate/frigate-common:rec_types",
|
||||||
|
"frigate/frigate-common:score_summary",
|
||||||
|
"frigate/frigate-common:util",
|
||||||
|
"frigate/frigate-common/src/main/scala/com/twitter/frigate/common/candidate",
|
||||||
|
"frigate/frigate-common/src/main/scala/com/twitter/frigate/common/experiments",
|
||||||
|
"frigate/frigate-common/src/main/scala/com/twitter/frigate/common/filter",
|
||||||
|
"frigate/frigate-common/src/main/scala/com/twitter/frigate/common/modules/store:semantic_core_stores",
|
||||||
|
"frigate/frigate-common/src/main/scala/com/twitter/frigate/common/store",
|
||||||
|
"frigate/frigate-common/src/main/scala/com/twitter/frigate/common/store/deviceinfo",
|
||||||
|
"frigate/frigate-common/src/main/scala/com/twitter/frigate/common/store/interests",
|
||||||
|
"frigate/frigate-common/src/main/scala/com/twitter/frigate/common/store/strato",
|
||||||
|
"frigate/push-mixer/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"geo/geo-prediction/src/main/thrift:local-viral-tweets-thrift-scala",
|
||||||
|
"geoduck/service/src/main/scala/com/twitter/geoduck/service/common/clientmodules",
|
||||||
|
"geoduck/util/country",
|
||||||
|
"gizmoduck/client/src/main/scala/com/twitter/gizmoduck/testusers/client",
|
||||||
|
"hermit/hermit-core:model-user_state",
|
||||||
|
"hermit/hermit-core:predicate",
|
||||||
|
"hermit/hermit-core:predicate-gizmoduck",
|
||||||
|
"hermit/hermit-core:predicate-scarecrow",
|
||||||
|
"hermit/hermit-core:predicate-socialgraph",
|
||||||
|
"hermit/hermit-core:predicate-tweetypie",
|
||||||
|
"hermit/hermit-core:store-labeled_push_recs",
|
||||||
|
"hermit/hermit-core:store-metastore",
|
||||||
|
"hermit/hermit-core:store-timezone",
|
||||||
|
"hermit/hermit-core:store-tweetypie",
|
||||||
|
"hermit/hermit-core/src/main/scala/com/twitter/hermit/constants",
|
||||||
|
"hermit/hermit-core/src/main/scala/com/twitter/hermit/model",
|
||||||
|
"hermit/hermit-core/src/main/scala/com/twitter/hermit/store",
|
||||||
|
"hermit/hermit-core/src/main/scala/com/twitter/hermit/store/common",
|
||||||
|
"hermit/hermit-core/src/main/scala/com/twitter/hermit/store/gizmoduck",
|
||||||
|
"hermit/hermit-core/src/main/scala/com/twitter/hermit/store/scarecrow",
|
||||||
|
"hermit/hermit-core/src/main/scala/com/twitter/hermit/store/semantic_core",
|
||||||
|
"hermit/hermit-core/src/main/scala/com/twitter/hermit/store/user_htl_session_store",
|
||||||
|
"hermit/hermit-core/src/main/scala/com/twitter/hermit/store/user_interest",
|
||||||
|
"hmli/hss/src/main/thrift/com/twitter/hss:thrift-scala",
|
||||||
|
"ibis2/service/src/main/scala/com/twitter/ibis2/lib",
|
||||||
|
"ibis2/service/src/main/thrift/com/twitter/ibis2/service:ibis2-service-scala",
|
||||||
|
"interests-service/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"interests_discovery/thrift/src/main/thrift:batch-thrift-scala",
|
||||||
|
"interests_discovery/thrift/src/main/thrift:service-thrift-scala",
|
||||||
|
"kujaku/thrift/src/main/thrift:domain-scala",
|
||||||
|
"live-video-timeline/client/src/main/scala/com/twitter/livevideo/timeline/client/v2",
|
||||||
|
"live-video-timeline/domain/src/main/scala/com/twitter/livevideo/timeline/domain",
|
||||||
|
"live-video-timeline/domain/src/main/scala/com/twitter/livevideo/timeline/domain/v2",
|
||||||
|
"live-video-timeline/thrift/src/main/thrift/com/twitter/livevideo/timeline:thrift-scala",
|
||||||
|
"live-video/common/src/main/scala/com/twitter/livevideo/common/domain/v2",
|
||||||
|
"live-video/common/src/main/scala/com/twitter/livevideo/common/ids",
|
||||||
|
"notifications-platform/inbound-notifications/src/main/thrift/com/twitter/inbound_notifications:exception-scala",
|
||||||
|
"notifications-platform/inbound-notifications/src/main/thrift/com/twitter/inbound_notifications:thrift-scala",
|
||||||
|
"notifications-platform/platform-lib/src/main/thrift/com/twitter/notifications/platform:custom-notification-actions-scala",
|
||||||
|
"notifications-platform/platform-lib/src/main/thrift/com/twitter/notifications/platform:thrift-scala",
|
||||||
|
"notifications-relevance/src/scala/com/twitter/nrel/heavyranker",
|
||||||
|
"notifications-relevance/src/scala/com/twitter/nrel/hydration/base",
|
||||||
|
"notifications-relevance/src/scala/com/twitter/nrel/hydration/frigate",
|
||||||
|
"notifications-relevance/src/scala/com/twitter/nrel/hydration/push",
|
||||||
|
"notifications-relevance/src/scala/com/twitter/nrel/lightranker",
|
||||||
|
"notificationservice/common/src/main/scala/com/twitter/notificationservice/genericfeedbackstore",
|
||||||
|
"notificationservice/common/src/main/scala/com/twitter/notificationservice/model:alias",
|
||||||
|
"notificationservice/common/src/main/scala/com/twitter/notificationservice/model/service",
|
||||||
|
"notificationservice/common/src/test/scala/com/twitter/notificationservice/mocks",
|
||||||
|
"notificationservice/scribe/src/main/scala/com/twitter/notificationservice/scribe/manhattan:mh_wrapper",
|
||||||
|
"notificationservice/thrift/src/main/thrift/com/twitter/notificationservice/api:thrift-scala",
|
||||||
|
"notificationservice/thrift/src/main/thrift/com/twitter/notificationservice/badgecount-api:thrift-scala",
|
||||||
|
"notificationservice/thrift/src/main/thrift/com/twitter/notificationservice/generic_notifications:thrift-scala",
|
||||||
|
"notifinfra/ni-lib/src/main/scala/com/twitter/ni/lib/logged_out_transform",
|
||||||
|
"observability/observability-manhattan-client/src/main/scala",
|
||||||
|
"onboarding/service/src/main/scala/com/twitter/onboarding/task/service/models/external",
|
||||||
|
"onboarding/service/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"people-discovery/api/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"periscope/api-proxy-thrift/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/module",
|
||||||
|
"product-mixer/core/src/main/scala/com/twitter/product_mixer/core/module/stringcenter",
|
||||||
|
"product-mixer/core/src/main/thrift/com/twitter/product_mixer/core:thrift-scala",
|
||||||
|
"qig-ranker/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"rux-ds/src/main/thrift/com/twitter/ruxds/jobs/user_past_aggregate:user_past_aggregate-scala",
|
||||||
|
"rux/common/src/main/scala/com/twitter/rux/common/encode",
|
||||||
|
"rux/common/thrift/src/main/thrift/rux-context:rux-context-scala",
|
||||||
|
"rux/common/thrift/src/main/thrift/strato:strato-scala",
|
||||||
|
"scribelib/marshallers/src/main/scala/com/twitter/scribelib/marshallers",
|
||||||
|
"scrooge/scrooge-core",
|
||||||
|
"scrooge/scrooge-serializer/src/main/scala",
|
||||||
|
"sensitive-ds/src/main/thrift/com/twitter/scio/nsfw_user_segmentation:nsfw_user_segmentation-scala",
|
||||||
|
"servo/decider/src/main/scala",
|
||||||
|
"servo/request/src/main/scala",
|
||||||
|
"servo/util/src/main/scala",
|
||||||
|
"src/java/com/twitter/ml/api:api-base",
|
||||||
|
"src/java/com/twitter/ml/prediction/core",
|
||||||
|
"src/scala/com/twitter/frigate/data_pipeline/common",
|
||||||
|
"src/scala/com/twitter/frigate/data_pipeline/embedding_cg:embedding_cg-test-user-ids",
|
||||||
|
"src/scala/com/twitter/frigate/data_pipeline/features_common",
|
||||||
|
"src/scala/com/twitter/frigate/news_article_recs/news_articles_metadata:thrift-scala",
|
||||||
|
"src/scala/com/twitter/frontpage/stream/util",
|
||||||
|
"src/scala/com/twitter/language/normalization",
|
||||||
|
"src/scala/com/twitter/ml/api/embedding",
|
||||||
|
"src/scala/com/twitter/ml/api/util:datarecord",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/catalog/entities/core",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/catalog/entities/magicrecs",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/catalog/features/core:aggregate",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/catalog/features/cuad:aggregate",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/catalog/features/embeddings",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/catalog/features/magicrecs:aggregate",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/catalog/features/topic_signals:aggregate",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/lib",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/lib/data",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/lib/dynamic",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/lib/entity",
|
||||||
|
"src/scala/com/twitter/ml/featurestore/lib/online",
|
||||||
|
"src/scala/com/twitter/recommendation/interests/discovery/core/config",
|
||||||
|
"src/scala/com/twitter/recommendation/interests/discovery/core/deploy",
|
||||||
|
"src/scala/com/twitter/recommendation/interests/discovery/core/model",
|
||||||
|
"src/scala/com/twitter/recommendation/interests/discovery/popgeo/deploy",
|
||||||
|
"src/scala/com/twitter/simclusters_v2/common",
|
||||||
|
"src/scala/com/twitter/storehaus_internal/manhattan",
|
||||||
|
"src/scala/com/twitter/storehaus_internal/manhattan/config",
|
||||||
|
"src/scala/com/twitter/storehaus_internal/memcache",
|
||||||
|
"src/scala/com/twitter/storehaus_internal/memcache/config",
|
||||||
|
"src/scala/com/twitter/storehaus_internal/util",
|
||||||
|
"src/scala/com/twitter/taxi/common",
|
||||||
|
"src/scala/com/twitter/taxi/config",
|
||||||
|
"src/scala/com/twitter/taxi/deploy",
|
||||||
|
"src/scala/com/twitter/taxi/trending/common",
|
||||||
|
"src/thrift/com/twitter/ads/adserver:adserver_rpc-scala",
|
||||||
|
"src/thrift/com/twitter/clientapp/gen:clientapp-scala",
|
||||||
|
"src/thrift/com/twitter/core_workflows/user_model:user_model-scala",
|
||||||
|
"src/thrift/com/twitter/escherbird/common:constants-scala",
|
||||||
|
"src/thrift/com/twitter/escherbird/metadata:megadata-scala",
|
||||||
|
"src/thrift/com/twitter/escherbird/metadata:metadata-service-scala",
|
||||||
|
"src/thrift/com/twitter/escherbird/search:search-service-scala",
|
||||||
|
"src/thrift/com/twitter/expandodo:only-scala",
|
||||||
|
"src/thrift/com/twitter/frigate:frigate-common-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate:frigate-ml-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate:frigate-notification-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate:frigate-secondary-accounts-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate:frigate-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate:frigate-user-media-representation-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate/data_pipeline:frigate-user-history-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate/dau_model:frigate-dau-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate/magic_events:frigate-magic-events-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate/magic_events/scribe:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate/pushcap:frigate-pushcap-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate/pushservice:frigate-pushservice-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate/scribe:frigate-scribe-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate/subscribed_search:frigate-subscribed-search-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/frigate/user_states:frigate-userstates-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/geoduck:geoduck-scala",
|
||||||
|
"src/thrift/com/twitter/gizmoduck:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/gizmoduck:user-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/hermit:hermit-scala",
|
||||||
|
"src/thrift/com/twitter/hermit/pop_geo:hermit-pop-geo-scala",
|
||||||
|
"src/thrift/com/twitter/hermit/stp:hermit-stp-scala",
|
||||||
|
"src/thrift/com/twitter/ibis:service-scala",
|
||||||
|
"src/thrift/com/twitter/manhattan:v1-scala",
|
||||||
|
"src/thrift/com/twitter/manhattan:v2-scala",
|
||||||
|
"src/thrift/com/twitter/ml/api:data-java",
|
||||||
|
"src/thrift/com/twitter/ml/api:data-scala",
|
||||||
|
"src/thrift/com/twitter/ml/featurestore/timelines:ml-features-timelines-scala",
|
||||||
|
"src/thrift/com/twitter/ml/featurestore/timelines:ml-features-timelines-strato",
|
||||||
|
"src/thrift/com/twitter/ml/prediction_service:prediction_service-java",
|
||||||
|
"src/thrift/com/twitter/permissions_storage:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/pink-floyd/thrift:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/recos:recos-common-scala",
|
||||||
|
"src/thrift/com/twitter/recos/user_tweet_entity_graph:user_tweet_entity_graph-scala",
|
||||||
|
"src/thrift/com/twitter/recos/user_user_graph:user_user_graph-scala",
|
||||||
|
"src/thrift/com/twitter/relevance/feature_store:feature_store-scala",
|
||||||
|
"src/thrift/com/twitter/search:earlybird-scala",
|
||||||
|
"src/thrift/com/twitter/search/common:features-scala",
|
||||||
|
"src/thrift/com/twitter/search/query_interaction_graph:query_interaction_graph-scala",
|
||||||
|
"src/thrift/com/twitter/search/query_interaction_graph/service:qig-service-scala",
|
||||||
|
"src/thrift/com/twitter/service/metastore/gen:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/service/scarecrow/gen:scarecrow-scala",
|
||||||
|
"src/thrift/com/twitter/service/scarecrow/gen:tiered-actions-scala",
|
||||||
|
"src/thrift/com/twitter/simclusters_v2:simclusters_v2-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/socialgraph:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/spam/rtf:safety-level-scala",
|
||||||
|
"src/thrift/com/twitter/timelinemixer:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/timelinemixer/server/internal:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/timelines/author_features/user_health:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/timelines/real_graph:real_graph-scala",
|
||||||
|
"src/thrift/com/twitter/timelinescorer:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/timelinescorer/server/internal:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/timelineservice/server/internal:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/timelineservice/server/suggests/logging:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/trends/common:common-scala",
|
||||||
|
"src/thrift/com/twitter/trends/trip_v1:trip-tweets-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/tweetypie:service-scala",
|
||||||
|
"src/thrift/com/twitter/tweetypie:tweet-scala",
|
||||||
|
"src/thrift/com/twitter/user_session_store:thrift-scala",
|
||||||
|
"src/thrift/com/twitter/wtf/candidate:wtf-candidate-scala",
|
||||||
|
"src/thrift/com/twitter/wtf/interest:interest-thrift-scala",
|
||||||
|
"src/thrift/com/twitter/wtf/scalding/common:thrift-scala",
|
||||||
|
"stitch/stitch-core",
|
||||||
|
"stitch/stitch-gizmoduck",
|
||||||
|
"stitch/stitch-socialgraph/src/main/scala",
|
||||||
|
"stitch/stitch-storehaus/src/main/scala",
|
||||||
|
"stitch/stitch-tweetypie/src/main/scala",
|
||||||
|
"storage/clients/manhattan/client/src/main/scala",
|
||||||
|
"strato/config/columns/clients:clients-strato-client",
|
||||||
|
"strato/config/columns/geo/user:user-strato-client",
|
||||||
|
"strato/config/columns/globe/curation:curation-strato-client",
|
||||||
|
"strato/config/columns/interests:interests-strato-client",
|
||||||
|
"strato/config/columns/ml/featureStore:featureStore-strato-client",
|
||||||
|
"strato/config/columns/notifications:notifications-strato-client",
|
||||||
|
"strato/config/columns/notifinfra:notifinfra-strato-client",
|
||||||
|
"strato/config/columns/periscope:periscope-strato-client",
|
||||||
|
"strato/config/columns/rux",
|
||||||
|
"strato/config/columns/rux:rux-strato-client",
|
||||||
|
"strato/config/columns/rux/open-app:open-app-strato-client",
|
||||||
|
"strato/config/columns/socialgraph/graphs:graphs-strato-client",
|
||||||
|
"strato/config/columns/socialgraph/service/soft_users:soft_users-strato-client",
|
||||||
|
"strato/config/columns/translation/service:service-strato-client",
|
||||||
|
"strato/config/columns/translation/service/platform:platform-strato-client",
|
||||||
|
"strato/config/columns/trends/trip:trip-strato-client",
|
||||||
|
"strato/config/src/thrift/com/twitter/strato/columns/frigate:logged-out-web-notifications-scala",
|
||||||
|
"strato/config/src/thrift/com/twitter/strato/columns/notifications:thrift-scala",
|
||||||
|
"strato/src/main/scala/com/twitter/strato/config",
|
||||||
|
"strato/src/main/scala/com/twitter/strato/response",
|
||||||
|
"thrift-web-forms",
|
||||||
|
"timeline-training-service/service/thrift/src/main/thrift:thrift-scala",
|
||||||
|
"timelines/src/main/scala/com/twitter/timelines/features/app",
|
||||||
|
"topic-social-proof/server/src/main/thrift:thrift-scala",
|
||||||
|
"topiclisting/topiclisting-core/src/main/scala/com/twitter/topiclisting",
|
||||||
|
"topiclisting/topiclisting-utt/src/main/scala/com/twitter/topiclisting/utt",
|
||||||
|
"trends/common/src/main/thrift/com/twitter/trends/common:thrift-scala",
|
||||||
|
"tweetypie/src/scala/com/twitter/tweetypie/tweettext",
|
||||||
|
"twitter-context/src/main/scala",
|
||||||
|
"twitter-server-internal",
|
||||||
|
"twitter-server/server/src/main/scala",
|
||||||
|
"twitter-text/lib/java/src/main/java/com/twitter/twittertext",
|
||||||
|
"twml/runtime/src/main/scala/com/twitter/deepbird/runtime/prediction_engine:prediction_engine_mkl",
|
||||||
|
"ubs/common/src/main/thrift/com/twitter/ubs:broadcast-thrift-scala",
|
||||||
|
"ubs/common/src/main/thrift/com/twitter/ubs:seller_application-thrift-scala",
|
||||||
|
"user_session_store/src/main/scala/com/twitter/user_session_store/impl/manhattan/readwrite",
|
||||||
|
"util-internal/scribe",
|
||||||
|
"util-internal/tunable/src/main/scala/com/twitter/util/tunable",
|
||||||
|
"util/util-app",
|
||||||
|
"util/util-hashing/src/main/scala",
|
||||||
|
"util/util-slf4j-api/src/main/scala",
|
||||||
|
"util/util-stats/src/main/scala",
|
||||||
|
"visibility/lib/src/main/scala/com/twitter/visibility/builder",
|
||||||
|
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/push_service",
|
||||||
|
"visibility/lib/src/main/scala/com/twitter/visibility/interfaces/spaces",
|
||||||
|
"visibility/lib/src/main/scala/com/twitter/visibility/util",
|
||||||
|
],
|
||||||
|
exports = [
|
||||||
|
"strato/config/src/thrift/com/twitter/strato/columns/frigate:logged-out-web-notifications-scala",
|
||||||
|
],
|
||||||
|
)
|
@ -0,0 +1,93 @@
|
|||||||
|
package com.twitter.frigate.pushservice
|
||||||
|
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import com.google.inject.Singleton
|
||||||
|
import com.twitter.finagle.mtls.authentication.ServiceIdentifier
|
||||||
|
import com.twitter.finagle.thrift.ClientId
|
||||||
|
import com.twitter.finatra.thrift.routing.ThriftWarmup
|
||||||
|
import com.twitter.util.logging.Logging
|
||||||
|
import com.twitter.inject.utils.Handler
|
||||||
|
import com.twitter.frigate.pushservice.{thriftscala => t}
|
||||||
|
import com.twitter.frigate.thriftscala.NotificationDisplayLocation
|
||||||
|
import com.twitter.util.Stopwatch
|
||||||
|
import com.twitter.scrooge.Request
|
||||||
|
import com.twitter.scrooge.Response
|
||||||
|
import com.twitter.util.Return
|
||||||
|
import com.twitter.util.Throw
|
||||||
|
import com.twitter.util.Try
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Warms up the refresh request path.
|
||||||
|
* If service is running as pushservice-send then the warmup does nothing.
|
||||||
|
*
|
||||||
|
* When making the warmup refresh requests we
|
||||||
|
* - Set skipFilters to true to execute as much of the request path as possible
|
||||||
|
* - Set darkWrite to true to prevent sending a push
|
||||||
|
*/
|
||||||
|
@Singleton
|
||||||
|
class PushMixerThriftServerWarmupHandler @Inject() (
|
||||||
|
warmup: ThriftWarmup,
|
||||||
|
serviceIdentifier: ServiceIdentifier)
|
||||||
|
extends Handler
|
||||||
|
with Logging {
|
||||||
|
|
||||||
|
private val clientId = ClientId("thrift-warmup-client")
|
||||||
|
|
||||||
|
def handle(): Unit = {
|
||||||
|
val refreshServices = Set(
|
||||||
|
"frigate-pushservice",
|
||||||
|
"frigate-pushservice-canary",
|
||||||
|
"frigate-pushservice-canary-control",
|
||||||
|
"frigate-pushservice-canary-treatment"
|
||||||
|
)
|
||||||
|
val isRefresh = refreshServices.contains(serviceIdentifier.service)
|
||||||
|
if (isRefresh && !serviceIdentifier.isLocal) refreshWarmup()
|
||||||
|
}
|
||||||
|
|
||||||
|
def refreshWarmup(): Unit = {
|
||||||
|
val elapsed = Stopwatch.start()
|
||||||
|
val testIds = Seq(
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
3
|
||||||
|
)
|
||||||
|
try {
|
||||||
|
clientId.asCurrent {
|
||||||
|
testIds.foreach { id =>
|
||||||
|
val warmupReq = warmupQuery(id)
|
||||||
|
info(s"Sending warm-up request to service with query: $warmupReq")
|
||||||
|
warmup.sendRequest(
|
||||||
|
method = t.PushService.Refresh,
|
||||||
|
req = Request(t.PushService.Refresh.Args(warmupReq)))(assertWarmupResponse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
case e: Throwable =>
|
||||||
|
error(e.getMessage, e)
|
||||||
|
}
|
||||||
|
info(s"Warm up complete. Time taken: ${elapsed().toString}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private def warmupQuery(userId: Long): t.RefreshRequest = {
|
||||||
|
t.RefreshRequest(
|
||||||
|
userId = userId,
|
||||||
|
notificationDisplayLocation = NotificationDisplayLocation.PushToMobileDevice,
|
||||||
|
context = Some(
|
||||||
|
t.PushContext(
|
||||||
|
skipFilters = Some(true),
|
||||||
|
darkWrite = Some(true)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def assertWarmupResponse(
|
||||||
|
result: Try[Response[t.PushService.Refresh.SuccessType]]
|
||||||
|
): Unit = {
|
||||||
|
result match {
|
||||||
|
case Return(_) => // ok
|
||||||
|
case Throw(exception) =>
|
||||||
|
warn("Error performing warm-up request.")
|
||||||
|
error(exception.getMessage, exception)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,193 @@
|
|||||||
|
package com.twitter.frigate.pushservice
|
||||||
|
|
||||||
|
import com.twitter.discovery.common.environment.modules.EnvironmentModule
|
||||||
|
import com.twitter.finagle.Filter
|
||||||
|
import com.twitter.finatra.annotations.DarkTrafficFilterType
|
||||||
|
import com.twitter.finatra.decider.modules.DeciderModule
|
||||||
|
import com.twitter.finatra.http.HttpServer
|
||||||
|
import com.twitter.finatra.http.filters.CommonFilters
|
||||||
|
import com.twitter.finatra.http.routing.HttpRouter
|
||||||
|
import com.twitter.finatra.mtls.http.{Mtls => HttpMtls}
|
||||||
|
import com.twitter.finatra.mtls.thriftmux.{Mtls => ThriftMtls}
|
||||||
|
import com.twitter.finatra.mtls.thriftmux.filters.MtlsServerSessionTrackerFilter
|
||||||
|
import com.twitter.finatra.thrift.ThriftServer
|
||||||
|
import com.twitter.finatra.thrift.filters.ExceptionMappingFilter
|
||||||
|
import com.twitter.finatra.thrift.filters.LoggingMDCFilter
|
||||||
|
import com.twitter.finatra.thrift.filters.StatsFilter
|
||||||
|
import com.twitter.finatra.thrift.filters.ThriftMDCFilter
|
||||||
|
import com.twitter.finatra.thrift.filters.TraceIdMDCFilter
|
||||||
|
import com.twitter.finatra.thrift.routing.ThriftRouter
|
||||||
|
import com.twitter.frigate.common.logger.MRLoggerGlobalVariables
|
||||||
|
import com.twitter.frigate.pushservice.controller.PushServiceController
|
||||||
|
import com.twitter.frigate.pushservice.module._
|
||||||
|
import com.twitter.inject.TwitterModule
|
||||||
|
import com.twitter.inject.annotations.Flags
|
||||||
|
import com.twitter.inject.thrift.modules.ThriftClientIdModule
|
||||||
|
import com.twitter.logging.BareFormatter
|
||||||
|
import com.twitter.logging.Level
|
||||||
|
import com.twitter.logging.LoggerFactory
|
||||||
|
import com.twitter.logging.{Logging => JLogging}
|
||||||
|
import com.twitter.logging.QueueingHandler
|
||||||
|
import com.twitter.logging.ScribeHandler
|
||||||
|
import com.twitter.product_mixer.core.module.product_mixer_flags.ProductMixerFlagModule
|
||||||
|
import com.twitter.product_mixer.core.module.ABDeciderModule
|
||||||
|
import com.twitter.product_mixer.core.module.FeatureSwitchesModule
|
||||||
|
import com.twitter.product_mixer.core.module.StratoClientModule
|
||||||
|
|
||||||
|
object PushServiceMain extends PushServiceFinatraServer
|
||||||
|
|
||||||
|
class PushServiceFinatraServer
|
||||||
|
extends ThriftServer
|
||||||
|
with ThriftMtls
|
||||||
|
with HttpServer
|
||||||
|
with HttpMtls
|
||||||
|
with JLogging {
|
||||||
|
|
||||||
|
override val name = "PushService"
|
||||||
|
|
||||||
|
override val modules: Seq[TwitterModule] = {
|
||||||
|
Seq(
|
||||||
|
ABDeciderModule,
|
||||||
|
DeciderModule,
|
||||||
|
FeatureSwitchesModule,
|
||||||
|
FilterModule,
|
||||||
|
FlagModule,
|
||||||
|
EnvironmentModule,
|
||||||
|
ThriftClientIdModule,
|
||||||
|
DeployConfigModule,
|
||||||
|
ProductMixerFlagModule,
|
||||||
|
StratoClientModule,
|
||||||
|
PushHandlerModule,
|
||||||
|
PushTargetUserBuilderModule,
|
||||||
|
PushServiceDarkTrafficModule,
|
||||||
|
LoggedOutPushTargetUserBuilderModule,
|
||||||
|
new ThriftWebFormsModule(this),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def configureThrift(router: ThriftRouter): Unit = {
|
||||||
|
router
|
||||||
|
.filter[ExceptionMappingFilter]
|
||||||
|
.filter[LoggingMDCFilter]
|
||||||
|
.filter[TraceIdMDCFilter]
|
||||||
|
.filter[ThriftMDCFilter]
|
||||||
|
.filter[MtlsServerSessionTrackerFilter]
|
||||||
|
.filter[StatsFilter]
|
||||||
|
.filter[Filter.TypeAgnostic, DarkTrafficFilterType]
|
||||||
|
.add[PushServiceController]
|
||||||
|
}
|
||||||
|
|
||||||
|
override def configureHttp(router: HttpRouter): Unit =
|
||||||
|
router
|
||||||
|
.filter[CommonFilters]
|
||||||
|
|
||||||
|
override protected def start(): Unit = {
|
||||||
|
MRLoggerGlobalVariables.setRequiredFlags(
|
||||||
|
traceLogFlag = injector.instance[Boolean](Flags.named(FlagModule.mrLoggerIsTraceAll.name)),
|
||||||
|
nthLogFlag = injector.instance[Boolean](Flags.named(FlagModule.mrLoggerNthLog.name)),
|
||||||
|
nthLogValFlag = injector.instance[Long](Flags.named(FlagModule.mrLoggerNthVal.name))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override protected def warmup(): Unit = {
|
||||||
|
handle[PushMixerThriftServerWarmupHandler]()
|
||||||
|
}
|
||||||
|
|
||||||
|
override protected def configureLoggerFactories(): Unit = {
|
||||||
|
loggerFactories.foreach { _() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override def loggerFactories: List[LoggerFactory] = {
|
||||||
|
val scribeScope = statsReceiver.scope("scribe")
|
||||||
|
List(
|
||||||
|
LoggerFactory(
|
||||||
|
level = Some(levelFlag()),
|
||||||
|
handlers = handlers
|
||||||
|
),
|
||||||
|
LoggerFactory(
|
||||||
|
node = "request_scribe",
|
||||||
|
level = Some(Level.INFO),
|
||||||
|
useParents = false,
|
||||||
|
handlers = QueueingHandler(
|
||||||
|
maxQueueSize = 10000,
|
||||||
|
handler = ScribeHandler(
|
||||||
|
category = "frigate_pushservice_log",
|
||||||
|
formatter = BareFormatter,
|
||||||
|
statsReceiver = scribeScope.scope("frigate_pushservice_log")
|
||||||
|
)
|
||||||
|
) :: Nil
|
||||||
|
),
|
||||||
|
LoggerFactory(
|
||||||
|
node = "notification_scribe",
|
||||||
|
level = Some(Level.INFO),
|
||||||
|
useParents = false,
|
||||||
|
handlers = QueueingHandler(
|
||||||
|
maxQueueSize = 10000,
|
||||||
|
handler = ScribeHandler(
|
||||||
|
category = "frigate_notifier",
|
||||||
|
formatter = BareFormatter,
|
||||||
|
statsReceiver = scribeScope.scope("frigate_notifier")
|
||||||
|
)
|
||||||
|
) :: Nil
|
||||||
|
),
|
||||||
|
LoggerFactory(
|
||||||
|
node = "push_scribe",
|
||||||
|
level = Some(Level.INFO),
|
||||||
|
useParents = false,
|
||||||
|
handlers = QueueingHandler(
|
||||||
|
maxQueueSize = 10000,
|
||||||
|
handler = ScribeHandler(
|
||||||
|
category = "test_frigate_push",
|
||||||
|
formatter = BareFormatter,
|
||||||
|
statsReceiver = scribeScope.scope("test_frigate_push")
|
||||||
|
)
|
||||||
|
) :: Nil
|
||||||
|
),
|
||||||
|
LoggerFactory(
|
||||||
|
node = "push_subsample_scribe",
|
||||||
|
level = Some(Level.INFO),
|
||||||
|
useParents = false,
|
||||||
|
handlers = QueueingHandler(
|
||||||
|
maxQueueSize = 2500,
|
||||||
|
handler = ScribeHandler(
|
||||||
|
category = "magicrecs_candidates_subsample_scribe",
|
||||||
|
maxMessagesPerTransaction = 250,
|
||||||
|
maxMessagesToBuffer = 2500,
|
||||||
|
formatter = BareFormatter,
|
||||||
|
statsReceiver = scribeScope.scope("magicrecs_candidates_subsample_scribe")
|
||||||
|
)
|
||||||
|
) :: Nil
|
||||||
|
),
|
||||||
|
LoggerFactory(
|
||||||
|
node = "mr_request_scribe",
|
||||||
|
level = Some(Level.INFO),
|
||||||
|
useParents = false,
|
||||||
|
handlers = QueueingHandler(
|
||||||
|
maxQueueSize = 2500,
|
||||||
|
handler = ScribeHandler(
|
||||||
|
category = "mr_request_scribe",
|
||||||
|
maxMessagesPerTransaction = 250,
|
||||||
|
maxMessagesToBuffer = 2500,
|
||||||
|
formatter = BareFormatter,
|
||||||
|
statsReceiver = scribeScope.scope("mr_request_scribe")
|
||||||
|
)
|
||||||
|
) :: Nil
|
||||||
|
),
|
||||||
|
LoggerFactory(
|
||||||
|
node = "high_quality_candidates_scribe",
|
||||||
|
level = Some(Level.INFO),
|
||||||
|
useParents = false,
|
||||||
|
handlers = QueueingHandler(
|
||||||
|
maxQueueSize = 2500,
|
||||||
|
handler = ScribeHandler(
|
||||||
|
category = "frigate_high_quality_candidates_log",
|
||||||
|
maxMessagesPerTransaction = 250,
|
||||||
|
maxMessagesToBuffer = 2500,
|
||||||
|
formatter = BareFormatter,
|
||||||
|
statsReceiver = scribeScope.scope("high_quality_candidates_scribe")
|
||||||
|
)
|
||||||
|
) :: Nil
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,323 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.contentrecommender.thriftscala.MetricTag
|
||||||
|
import com.twitter.cr_mixer.thriftscala.CrMixerTweetRequest
|
||||||
|
import com.twitter.cr_mixer.thriftscala.NotificationsContext
|
||||||
|
import com.twitter.cr_mixer.thriftscala.Product
|
||||||
|
import com.twitter.cr_mixer.thriftscala.ProductContext
|
||||||
|
import com.twitter.cr_mixer.thriftscala.{MetricTag => CrMixerMetricTag}
|
||||||
|
import com.twitter.finagle.stats.Stat
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.AlgorithmScore
|
||||||
|
import com.twitter.frigate.common.base.CandidateSource
|
||||||
|
import com.twitter.frigate.common.base.CandidateSourceEligible
|
||||||
|
import com.twitter.frigate.common.base.CrMixerCandidate
|
||||||
|
import com.twitter.frigate.common.base.TopicCandidate
|
||||||
|
import com.twitter.frigate.common.base.TopicProofTweetCandidate
|
||||||
|
import com.twitter.frigate.common.base.TweetCandidate
|
||||||
|
import com.twitter.frigate.common.predicate.CommonOutNetworkTweetCandidatesSourcePredicates.filterOutInNetworkTweets
|
||||||
|
import com.twitter.frigate.common.predicate.CommonOutNetworkTweetCandidatesSourcePredicates.filterOutReplyTweet
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.params.PushParams
|
||||||
|
import com.twitter.frigate.pushservice.store.CrMixerTweetStore
|
||||||
|
import com.twitter.frigate.pushservice.store.UttEntityHydrationStore
|
||||||
|
import com.twitter.frigate.pushservice.util.AdaptorUtils
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.TopicsUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.TweetWithTopicProof
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.hermit.predicate.socialgraph.RelationEdge
|
||||||
|
import com.twitter.product_mixer.core.thriftscala.ClientContext
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.topiclisting.utt.LocalizedEntity
|
||||||
|
import com.twitter.tsp.thriftscala.TopicSocialProofRequest
|
||||||
|
import com.twitter.tsp.thriftscala.TopicSocialProofResponse
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import scala.collection.Map
|
||||||
|
|
||||||
|
case class ContentRecommenderMixerAdaptor(
|
||||||
|
crMixerTweetStore: CrMixerTweetStore,
|
||||||
|
tweetyPieStore: ReadableStore[Long, TweetyPieResult],
|
||||||
|
edgeStore: ReadableStore[RelationEdge, Boolean],
|
||||||
|
topicSocialProofServiceStore: ReadableStore[TopicSocialProofRequest, TopicSocialProofResponse],
|
||||||
|
uttEntityHydrationStore: UttEntityHydrationStore,
|
||||||
|
globalStats: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
|
||||||
|
override val name: String = this.getClass.getSimpleName
|
||||||
|
|
||||||
|
private[this] val stats = globalStats.scope("ContentRecommenderMixerAdaptor")
|
||||||
|
private[this] val numOfValidAuthors = stats.stat("num_of_valid_authors")
|
||||||
|
private[this] val numOutOfMaximumDropped = stats.stat("dropped_due_out_of_maximum")
|
||||||
|
private[this] val totalInputRecs = stats.counter("input_recs")
|
||||||
|
private[this] val totalOutputRecs = stats.stat("output_recs")
|
||||||
|
private[this] val totalRequests = stats.counter("total_requests")
|
||||||
|
private[this] val nonReplyTweetsCounter = stats.counter("non_reply_tweets")
|
||||||
|
private[this] val totalOutNetworkRecs = stats.counter("out_network_tweets")
|
||||||
|
private[this] val totalInNetworkRecs = stats.counter("in_network_tweets")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds OON raw candidates based on input OON Tweets
|
||||||
|
*/
|
||||||
|
def buildOONRawCandidates(
|
||||||
|
inputTarget: Target,
|
||||||
|
oonTweets: Seq[TweetyPieResult],
|
||||||
|
tweetScoreMap: Map[Long, Double],
|
||||||
|
tweetIdToTagsMap: Map[Long, Seq[CrMixerMetricTag]],
|
||||||
|
maxNumOfCandidates: Int
|
||||||
|
): Option[Seq[RawCandidate]] = {
|
||||||
|
val cands = oonTweets.flatMap { tweetResult =>
|
||||||
|
val tweetId = tweetResult.tweet.id
|
||||||
|
generateOONRawCandidate(
|
||||||
|
inputTarget,
|
||||||
|
tweetId,
|
||||||
|
Some(tweetResult),
|
||||||
|
tweetScoreMap,
|
||||||
|
tweetIdToTagsMap
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val candidates = restrict(
|
||||||
|
maxNumOfCandidates,
|
||||||
|
cands,
|
||||||
|
numOutOfMaximumDropped,
|
||||||
|
totalOutputRecs
|
||||||
|
)
|
||||||
|
|
||||||
|
Some(candidates)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds a single RawCandidate With TopicProofTweetCandidate
|
||||||
|
*/
|
||||||
|
def buildTopicTweetRawCandidate(
|
||||||
|
inputTarget: Target,
|
||||||
|
tweetWithTopicProof: TweetWithTopicProof,
|
||||||
|
localizedEntity: LocalizedEntity,
|
||||||
|
tags: Option[Seq[MetricTag]],
|
||||||
|
): RawCandidate with TopicProofTweetCandidate = {
|
||||||
|
new RawCandidate with TopicProofTweetCandidate {
|
||||||
|
override def target: Target = inputTarget
|
||||||
|
override def topicListingSetting: Option[String] = Some(
|
||||||
|
tweetWithTopicProof.topicListingSetting)
|
||||||
|
override def tweetId: Long = tweetWithTopicProof.tweetId
|
||||||
|
override def tweetyPieResult: Option[TweetyPieResult] = Some(
|
||||||
|
tweetWithTopicProof.tweetyPieResult)
|
||||||
|
override def semanticCoreEntityId: Option[Long] = Some(tweetWithTopicProof.topicId)
|
||||||
|
override def localizedUttEntity: Option[LocalizedEntity] = Some(localizedEntity)
|
||||||
|
override def algorithmCR: Option[String] = tweetWithTopicProof.algorithmCR
|
||||||
|
override def tagsCR: Option[Seq[MetricTag]] = tags
|
||||||
|
override def isOutOfNetwork: Boolean = tweetWithTopicProof.isOON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes a group of TopicTweets and transforms them into RawCandidates
|
||||||
|
*/
|
||||||
|
def buildTopicTweetRawCandidates(
|
||||||
|
inputTarget: Target,
|
||||||
|
topicProofCandidates: Seq[TweetWithTopicProof],
|
||||||
|
tweetIdToTagsMap: Map[Long, Seq[CrMixerMetricTag]],
|
||||||
|
maxNumberOfCands: Int
|
||||||
|
): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
val semanticCoreEntityIds = topicProofCandidates
|
||||||
|
.map(_.topicId)
|
||||||
|
.toSet
|
||||||
|
|
||||||
|
TopicsUtil
|
||||||
|
.getLocalizedEntityMap(inputTarget, semanticCoreEntityIds, uttEntityHydrationStore)
|
||||||
|
.map { localizedEntityMap =>
|
||||||
|
val rawCandidates = topicProofCandidates.collect {
|
||||||
|
case topicSocialProof: TweetWithTopicProof
|
||||||
|
if localizedEntityMap.contains(topicSocialProof.topicId) =>
|
||||||
|
// Once we deprecate CR calls, we should replace this code to use the CrMixerMetricTag
|
||||||
|
val tags = tweetIdToTagsMap.get(topicSocialProof.tweetId).map {
|
||||||
|
_.flatMap { tag => MetricTag.get(tag.value) }
|
||||||
|
}
|
||||||
|
buildTopicTweetRawCandidate(
|
||||||
|
inputTarget,
|
||||||
|
topicSocialProof,
|
||||||
|
localizedEntityMap(topicSocialProof.topicId),
|
||||||
|
tags
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val candResult = restrict(
|
||||||
|
maxNumberOfCands,
|
||||||
|
rawCandidates,
|
||||||
|
numOutOfMaximumDropped,
|
||||||
|
totalOutputRecs
|
||||||
|
)
|
||||||
|
|
||||||
|
Some(candResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def generateOONRawCandidate(
|
||||||
|
inputTarget: Target,
|
||||||
|
id: Long,
|
||||||
|
result: Option[TweetyPieResult],
|
||||||
|
tweetScoreMap: Map[Long, Double],
|
||||||
|
tweetIdToTagsMap: Map[Long, Seq[CrMixerMetricTag]]
|
||||||
|
): Option[RawCandidate with TweetCandidate] = {
|
||||||
|
val tagsFromCR = tweetIdToTagsMap.get(id).map { _.flatMap { tag => MetricTag.get(tag.value) } }
|
||||||
|
val candidate = new RawCandidate with CrMixerCandidate with TopicCandidate with AlgorithmScore {
|
||||||
|
override val tweetId = id
|
||||||
|
override val target = inputTarget
|
||||||
|
override val tweetyPieResult = result
|
||||||
|
override val localizedUttEntity = None
|
||||||
|
override val semanticCoreEntityId = None
|
||||||
|
override def commonRecType =
|
||||||
|
getMediaBasedCRT(
|
||||||
|
CommonRecommendationType.TwistlyTweet,
|
||||||
|
CommonRecommendationType.TwistlyPhoto,
|
||||||
|
CommonRecommendationType.TwistlyVideo)
|
||||||
|
override def tagsCR = tagsFromCR
|
||||||
|
override def algorithmScore = tweetScoreMap.get(id)
|
||||||
|
override def algorithmCR = None
|
||||||
|
}
|
||||||
|
Some(candidate)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def restrict(
|
||||||
|
maxNumToReturn: Int,
|
||||||
|
candidates: Seq[RawCandidate],
|
||||||
|
numOutOfMaximumDropped: Stat,
|
||||||
|
totalOutputRecs: Stat
|
||||||
|
): Seq[RawCandidate] = {
|
||||||
|
val newCandidates = candidates.take(maxNumToReturn)
|
||||||
|
val numDropped = candidates.length - newCandidates.length
|
||||||
|
numOutOfMaximumDropped.add(numDropped)
|
||||||
|
totalOutputRecs.add(newCandidates.size)
|
||||||
|
newCandidates
|
||||||
|
}
|
||||||
|
|
||||||
|
private def buildCrMixerRequest(
|
||||||
|
target: Target,
|
||||||
|
countryCode: Option[String],
|
||||||
|
language: Option[String],
|
||||||
|
seenTweets: Seq[Long]
|
||||||
|
): CrMixerTweetRequest = {
|
||||||
|
CrMixerTweetRequest(
|
||||||
|
clientContext = ClientContext(
|
||||||
|
userId = Some(target.targetId),
|
||||||
|
countryCode = countryCode,
|
||||||
|
languageCode = language
|
||||||
|
),
|
||||||
|
product = Product.Notifications,
|
||||||
|
productContext = Some(ProductContext.NotificationsContext(NotificationsContext())),
|
||||||
|
excludedTweetIds = Some(seenTweets)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def selectCandidatesToSendBasedOnSettings(
|
||||||
|
isRecommendationsEligible: Boolean,
|
||||||
|
isTopicsEligible: Boolean,
|
||||||
|
oonRawCandidates: Option[Seq[RawCandidate]],
|
||||||
|
topicTweetCandidates: Option[Seq[RawCandidate]]
|
||||||
|
): Option[Seq[RawCandidate]] = {
|
||||||
|
if (isRecommendationsEligible && isTopicsEligible) {
|
||||||
|
Some(topicTweetCandidates.getOrElse(Seq.empty) ++ oonRawCandidates.getOrElse(Seq.empty))
|
||||||
|
} else if (isRecommendationsEligible) {
|
||||||
|
oonRawCandidates
|
||||||
|
} else if (isTopicsEligible) {
|
||||||
|
topicTweetCandidates
|
||||||
|
} else None
|
||||||
|
}
|
||||||
|
|
||||||
|
override def get(target: Target): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
target.seenTweetIds,
|
||||||
|
target.countryCode,
|
||||||
|
target.inferredUserDeviceLanguage,
|
||||||
|
PushDeviceUtil.isTopicsEligible(target),
|
||||||
|
PushDeviceUtil.isRecommendationsEligible(target)
|
||||||
|
).flatMap {
|
||||||
|
case (seenTweets, countryCode, language, isTopicsEligible, isRecommendationsEligible) =>
|
||||||
|
val request = buildCrMixerRequest(target, countryCode, language, seenTweets)
|
||||||
|
crMixerTweetStore.getTweetRecommendations(request).flatMap {
|
||||||
|
case Some(response) =>
|
||||||
|
totalInputRecs.incr(response.tweets.size)
|
||||||
|
totalRequests.incr()
|
||||||
|
AdaptorUtils
|
||||||
|
.getTweetyPieResults(
|
||||||
|
response.tweets.map(_.tweetId).toSet,
|
||||||
|
tweetyPieStore).flatMap { tweetyPieResultMap =>
|
||||||
|
filterOutInNetworkTweets(
|
||||||
|
target,
|
||||||
|
filterOutReplyTweet(tweetyPieResultMap.toMap, nonReplyTweetsCounter),
|
||||||
|
edgeStore,
|
||||||
|
numOfValidAuthors).flatMap {
|
||||||
|
outNetworkTweetsWithId: Seq[(Long, TweetyPieResult)] =>
|
||||||
|
totalOutNetworkRecs.incr(outNetworkTweetsWithId.size)
|
||||||
|
totalInNetworkRecs.incr(response.tweets.size - outNetworkTweetsWithId.size)
|
||||||
|
val outNetworkTweets: Seq[TweetyPieResult] = outNetworkTweetsWithId.map {
|
||||||
|
case (_, tweetyPieResult) => tweetyPieResult
|
||||||
|
}
|
||||||
|
|
||||||
|
val tweetIdToTagsMap = response.tweets.map { tweet =>
|
||||||
|
tweet.tweetId -> tweet.metricTags.getOrElse(Seq.empty)
|
||||||
|
}.toMap
|
||||||
|
|
||||||
|
val tweetScoreMap = response.tweets.map { tweet =>
|
||||||
|
tweet.tweetId -> tweet.score
|
||||||
|
}.toMap
|
||||||
|
|
||||||
|
val maxNumOfCandidates =
|
||||||
|
target.params(PushFeatureSwitchParams.NumberOfMaxCrMixerCandidatesParam)
|
||||||
|
|
||||||
|
val oonRawCandidates =
|
||||||
|
buildOONRawCandidates(
|
||||||
|
target,
|
||||||
|
outNetworkTweets,
|
||||||
|
tweetScoreMap,
|
||||||
|
tweetIdToTagsMap,
|
||||||
|
maxNumOfCandidates)
|
||||||
|
|
||||||
|
TopicsUtil
|
||||||
|
.getTopicSocialProofs(
|
||||||
|
target,
|
||||||
|
outNetworkTweets,
|
||||||
|
topicSocialProofServiceStore,
|
||||||
|
edgeStore,
|
||||||
|
PushFeatureSwitchParams.TopicProofTweetCandidatesTopicScoreThreshold).flatMap {
|
||||||
|
tweetsWithTopicProof =>
|
||||||
|
buildTopicTweetRawCandidates(
|
||||||
|
target,
|
||||||
|
tweetsWithTopicProof,
|
||||||
|
tweetIdToTagsMap,
|
||||||
|
maxNumOfCandidates)
|
||||||
|
}.map { topicTweetCandidates =>
|
||||||
|
selectCandidatesToSendBasedOnSettings(
|
||||||
|
isRecommendationsEligible,
|
||||||
|
isTopicsEligible,
|
||||||
|
oonRawCandidates,
|
||||||
|
topicTweetCandidates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case _ => Future.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For a user to be available the following news to happen
|
||||||
|
*/
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
PushDeviceUtil.isRecommendationsEligible(target),
|
||||||
|
PushDeviceUtil.isTopicsEligible(target)
|
||||||
|
).map {
|
||||||
|
case (isRecommendationsEligible, isTopicsEligible) =>
|
||||||
|
(isRecommendationsEligible || isTopicsEligible) &&
|
||||||
|
target.params(PushParams.ContentRecommenderMixerAdaptorDecider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,293 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.Stat
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base._
|
||||||
|
import com.twitter.frigate.common.candidate._
|
||||||
|
import com.twitter.frigate.common.predicate.CommonOutNetworkTweetCandidatesSourcePredicates.filterOutReplyTweet
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.params.PushParams
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.hermit.store.tweetypie.UserTweet
|
||||||
|
import com.twitter.recos.recos_common.thriftscala.SocialProofType
|
||||||
|
import com.twitter.search.common.features.thriftscala.ThriftSearchResultFeatures
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.timelines.configapi.Param
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import com.twitter.util.Time
|
||||||
|
import scala.collection.Map
|
||||||
|
|
||||||
|
case class EarlyBirdFirstDegreeCandidateAdaptor(
|
||||||
|
earlyBirdFirstDegreeCandidates: CandidateSource[
|
||||||
|
EarlybirdCandidateSource.Query,
|
||||||
|
EarlybirdCandidate
|
||||||
|
],
|
||||||
|
tweetyPieStore: ReadableStore[Long, TweetyPieResult],
|
||||||
|
tweetyPieStoreNoVF: ReadableStore[Long, TweetyPieResult],
|
||||||
|
userTweetTweetyPieStore: ReadableStore[UserTweet, TweetyPieResult],
|
||||||
|
maxResultsParam: Param[Int],
|
||||||
|
globalStats: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
|
||||||
|
type EBCandidate = EarlybirdCandidate with TweetDetails
|
||||||
|
private val stats = globalStats.scope("EarlyBirdFirstDegreeAdaptor")
|
||||||
|
private val earlyBirdCandsStat: Stat = stats.stat("early_bird_cands_dist")
|
||||||
|
private val emptyEarlyBirdCands = stats.counter("empty_early_bird_candidates")
|
||||||
|
private val seedSetEmpty = stats.counter("empty_seedset")
|
||||||
|
private val seenTweetsStat = stats.stat("filtered_by_seen_tweets")
|
||||||
|
private val emptyTweetyPieResult = stats.stat("empty_tweetypie_result")
|
||||||
|
private val nonReplyTweetsCounter = stats.counter("non_reply_tweets")
|
||||||
|
private val enableRetweets = stats.counter("enable_retweets")
|
||||||
|
private val f1withoutSocialContexts = stats.counter("f1_without_social_context")
|
||||||
|
private val userTweetTweetyPieStoreCounter = stats.counter("user_tweet_tweetypie_store")
|
||||||
|
|
||||||
|
override val name: String = earlyBirdFirstDegreeCandidates.name
|
||||||
|
|
||||||
|
private def getAllSocialContextActions(
|
||||||
|
socialProofTypes: Seq[(SocialProofType, Seq[Long])]
|
||||||
|
): Seq[SocialContextAction] = {
|
||||||
|
socialProofTypes.flatMap {
|
||||||
|
case (SocialProofType.Favorite, scIds) =>
|
||||||
|
scIds.map { scId =>
|
||||||
|
SocialContextAction(
|
||||||
|
scId,
|
||||||
|
Time.now.inMilliseconds,
|
||||||
|
socialContextActionType = Some(SocialContextActionType.Favorite)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case (SocialProofType.Retweet, scIds) =>
|
||||||
|
scIds.map { scId =>
|
||||||
|
SocialContextAction(
|
||||||
|
scId,
|
||||||
|
Time.now.inMilliseconds,
|
||||||
|
socialContextActionType = Some(SocialContextActionType.Retweet)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case (SocialProofType.Reply, scIds) =>
|
||||||
|
scIds.map { scId =>
|
||||||
|
SocialContextAction(
|
||||||
|
scId,
|
||||||
|
Time.now.inMilliseconds,
|
||||||
|
socialContextActionType = Some(SocialContextActionType.Reply)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case (SocialProofType.Tweet, scIds) =>
|
||||||
|
scIds.map { scId =>
|
||||||
|
SocialContextAction(
|
||||||
|
scId,
|
||||||
|
Time.now.inMilliseconds,
|
||||||
|
socialContextActionType = Some(SocialContextActionType.Tweet)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case _ => Nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def generateRetweetCandidate(
|
||||||
|
inputTarget: Target,
|
||||||
|
candidate: EBCandidate,
|
||||||
|
scIds: Seq[Long],
|
||||||
|
socialProofTypes: Seq[(SocialProofType, Seq[Long])]
|
||||||
|
): RawCandidate = {
|
||||||
|
val scActions = scIds.map { scId => SocialContextAction(scId, Time.now.inMilliseconds) }
|
||||||
|
new RawCandidate with TweetRetweetCandidate with EarlybirdTweetFeatures {
|
||||||
|
override val socialContextActions = scActions
|
||||||
|
override val socialContextAllTypeActions = getAllSocialContextActions(socialProofTypes)
|
||||||
|
override val tweetId = candidate.tweetId
|
||||||
|
override val target = inputTarget
|
||||||
|
override val tweetyPieResult = candidate.tweetyPieResult
|
||||||
|
override val features = candidate.features
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def generateF1CandidateWithoutSocialContext(
|
||||||
|
inputTarget: Target,
|
||||||
|
candidate: EBCandidate
|
||||||
|
): RawCandidate = {
|
||||||
|
f1withoutSocialContexts.incr()
|
||||||
|
new RawCandidate with F1FirstDegree with EarlybirdTweetFeatures {
|
||||||
|
override val tweetId = candidate.tweetId
|
||||||
|
override val target = inputTarget
|
||||||
|
override val tweetyPieResult = candidate.tweetyPieResult
|
||||||
|
override val features = candidate.features
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def generateEarlyBirdCandidate(
|
||||||
|
id: Long,
|
||||||
|
result: Option[TweetyPieResult],
|
||||||
|
ebFeatures: Option[ThriftSearchResultFeatures]
|
||||||
|
): EBCandidate = {
|
||||||
|
new EarlybirdCandidate with TweetDetails {
|
||||||
|
override val tweetyPieResult: Option[TweetyPieResult] = result
|
||||||
|
override val tweetId: Long = id
|
||||||
|
override val features: Option[ThriftSearchResultFeatures] = ebFeatures
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def filterOutSeenTweets(seenTweetIds: Seq[Long], inputTweetIds: Seq[Long]): Seq[Long] = {
|
||||||
|
inputTweetIds.filterNot(seenTweetIds.contains)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def filterInvalidTweets(
|
||||||
|
tweetIds: Seq[Long],
|
||||||
|
target: Target
|
||||||
|
): Future[Seq[(Long, TweetyPieResult)]] = {
|
||||||
|
|
||||||
|
val resMap = {
|
||||||
|
if (target.params(PushFeatureSwitchParams.EnableF1FromProtectedTweetAuthors)) {
|
||||||
|
userTweetTweetyPieStoreCounter.incr()
|
||||||
|
val keys = tweetIds.map { tweetId =>
|
||||||
|
UserTweet(tweetId, Some(target.targetId))
|
||||||
|
}
|
||||||
|
|
||||||
|
userTweetTweetyPieStore
|
||||||
|
.multiGet(keys.toSet).map {
|
||||||
|
case (userTweet, resultFut) =>
|
||||||
|
userTweet.tweetId -> resultFut
|
||||||
|
}.toMap
|
||||||
|
} else {
|
||||||
|
(target.params(PushFeatureSwitchParams.EnableVFInTweetypie) match {
|
||||||
|
case true => tweetyPieStore
|
||||||
|
case false => tweetyPieStoreNoVF
|
||||||
|
}).multiGet(tweetIds.toSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Future.collect(resMap).map { tweetyPieResultMap =>
|
||||||
|
val cands = filterOutReplyTweet(tweetyPieResultMap, nonReplyTweetsCounter).collect {
|
||||||
|
case (id: Long, Some(result)) =>
|
||||||
|
id -> result
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyTweetyPieResult.add(tweetyPieResultMap.size - cands.size)
|
||||||
|
cands.toSeq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getEBRetweetCandidates(
|
||||||
|
inputTarget: Target,
|
||||||
|
retweets: Seq[(Long, TweetyPieResult)]
|
||||||
|
): Seq[RawCandidate] = {
|
||||||
|
retweets.flatMap {
|
||||||
|
case (_, tweetypieResult) =>
|
||||||
|
tweetypieResult.tweet.coreData.flatMap { coreData =>
|
||||||
|
tweetypieResult.sourceTweet.map { sourceTweet =>
|
||||||
|
val tweetId = sourceTweet.id
|
||||||
|
val scId = coreData.userId
|
||||||
|
val socialProofTypes = Seq((SocialProofType.Retweet, Seq(scId)))
|
||||||
|
val candidate = generateEarlyBirdCandidate(
|
||||||
|
tweetId,
|
||||||
|
Some(TweetyPieResult(sourceTweet, None, None)),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
generateRetweetCandidate(
|
||||||
|
inputTarget,
|
||||||
|
candidate,
|
||||||
|
Seq(scId),
|
||||||
|
socialProofTypes
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getEBFirstDegreeCands(
|
||||||
|
tweets: Seq[(Long, TweetyPieResult)],
|
||||||
|
ebTweetIdMap: Map[Long, Option[ThriftSearchResultFeatures]]
|
||||||
|
): Seq[EBCandidate] = {
|
||||||
|
tweets.map {
|
||||||
|
case (id, tweetypieResult) =>
|
||||||
|
val features = ebTweetIdMap.getOrElse(id, None)
|
||||||
|
generateEarlyBirdCandidate(id, Some(tweetypieResult), features)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a combination of raw candidates made of: f1 recs, topic social proof recs, sc recs and retweet candidates
|
||||||
|
*/
|
||||||
|
def buildRawCandidates(
|
||||||
|
inputTarget: Target,
|
||||||
|
firstDegreeCandidates: Seq[EBCandidate],
|
||||||
|
retweetCandidates: Seq[RawCandidate]
|
||||||
|
): Seq[RawCandidate] = {
|
||||||
|
val hydratedF1Recs =
|
||||||
|
firstDegreeCandidates.map(generateF1CandidateWithoutSocialContext(inputTarget, _))
|
||||||
|
hydratedF1Recs ++ retweetCandidates
|
||||||
|
}
|
||||||
|
|
||||||
|
override def get(inputTarget: Target): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
inputTarget.seedsWithWeight.flatMap { seedsetOpt =>
|
||||||
|
val seedsetMap = seedsetOpt.getOrElse(Map.empty)
|
||||||
|
|
||||||
|
if (seedsetMap.isEmpty) {
|
||||||
|
seedSetEmpty.incr()
|
||||||
|
Future.None
|
||||||
|
} else {
|
||||||
|
val maxResultsToReturn = inputTarget.params(maxResultsParam)
|
||||||
|
val maxTweetAge = inputTarget.params(PushFeatureSwitchParams.F1CandidateMaxTweetAgeParam)
|
||||||
|
val earlybirdQuery = EarlybirdCandidateSource.Query(
|
||||||
|
maxNumResultsToReturn = maxResultsToReturn,
|
||||||
|
seedset = seedsetMap,
|
||||||
|
maxConsecutiveResultsByTheSameUser = Some(1),
|
||||||
|
maxTweetAge = maxTweetAge,
|
||||||
|
disableTimelinesMLModel = false,
|
||||||
|
searcherId = Some(inputTarget.targetId),
|
||||||
|
isProtectTweetsEnabled =
|
||||||
|
inputTarget.params(PushFeatureSwitchParams.EnableF1FromProtectedTweetAuthors),
|
||||||
|
followedUserIds = Some(seedsetMap.keySet.toSeq)
|
||||||
|
)
|
||||||
|
|
||||||
|
Future
|
||||||
|
.join(inputTarget.seenTweetIds, earlyBirdFirstDegreeCandidates.get(earlybirdQuery))
|
||||||
|
.flatMap {
|
||||||
|
case (seenTweetIds, Some(candidates)) =>
|
||||||
|
earlyBirdCandsStat.add(candidates.size)
|
||||||
|
|
||||||
|
val ebTweetIdMap = candidates.map { cand => cand.tweetId -> cand.features }.toMap
|
||||||
|
|
||||||
|
val ebTweetIds = ebTweetIdMap.keys.toSeq
|
||||||
|
|
||||||
|
val tweetIds = filterOutSeenTweets(seenTweetIds, ebTweetIds)
|
||||||
|
seenTweetsStat.add(ebTweetIds.size - tweetIds.size)
|
||||||
|
|
||||||
|
filterInvalidTweets(tweetIds, inputTarget)
|
||||||
|
.map { validTweets =>
|
||||||
|
val (retweets, tweets) = validTweets.partition {
|
||||||
|
case (_, tweetypieResult) =>
|
||||||
|
tweetypieResult.sourceTweet.isDefined
|
||||||
|
}
|
||||||
|
|
||||||
|
val firstDegreeCandidates = getEBFirstDegreeCands(tweets, ebTweetIdMap)
|
||||||
|
|
||||||
|
val retweetCandidates = {
|
||||||
|
if (inputTarget.params(PushParams.EarlyBirdSCBasedCandidatesParam) &&
|
||||||
|
inputTarget.params(PushParams.MRTweetRetweetRecsParam)) {
|
||||||
|
enableRetweets.incr()
|
||||||
|
getEBRetweetCandidates(inputTarget, retweets)
|
||||||
|
} else Nil
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(
|
||||||
|
buildRawCandidates(
|
||||||
|
inputTarget,
|
||||||
|
firstDegreeCandidates,
|
||||||
|
retweetCandidates
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
case _ =>
|
||||||
|
emptyEarlyBirdCands.incr()
|
||||||
|
Future.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
PushDeviceUtil.isRecommendationsEligible(target)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.explore_ranker.thriftscala.ExploreRankerProductResponse
|
||||||
|
import com.twitter.explore_ranker.thriftscala.ExploreRankerRequest
|
||||||
|
import com.twitter.explore_ranker.thriftscala.ExploreRankerResponse
|
||||||
|
import com.twitter.explore_ranker.thriftscala.ExploreRecommendation
|
||||||
|
import com.twitter.explore_ranker.thriftscala.ImmersiveRecsResponse
|
||||||
|
import com.twitter.explore_ranker.thriftscala.ImmersiveRecsResult
|
||||||
|
import com.twitter.explore_ranker.thriftscala.NotificationsVideoRecs
|
||||||
|
import com.twitter.explore_ranker.thriftscala.Product
|
||||||
|
import com.twitter.explore_ranker.thriftscala.ProductContext
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.CandidateSource
|
||||||
|
import com.twitter.frigate.common.base.CandidateSourceEligible
|
||||||
|
import com.twitter.frigate.common.base.OutOfNetworkTweetCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.util.AdaptorUtils
|
||||||
|
import com.twitter.frigate.pushservice.util.MediaCRT
|
||||||
|
import com.twitter.frigate.pushservice.util.PushAdaptorUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.product_mixer.core.thriftscala.ClientContext
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
case class ExploreVideoTweetCandidateAdaptor(
|
||||||
|
exploreRankerStore: ReadableStore[ExploreRankerRequest, ExploreRankerResponse],
|
||||||
|
tweetyPieStore: ReadableStore[Long, TweetyPieResult],
|
||||||
|
globalStats: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
|
||||||
|
override def name: String = this.getClass.getSimpleName
|
||||||
|
private[this] val stats = globalStats.scope("ExploreVideoTweetCandidateAdaptor")
|
||||||
|
private[this] val totalInputRecs = stats.stat("input_recs")
|
||||||
|
private[this] val totalRequests = stats.counter("total_requests")
|
||||||
|
private[this] val totalEmptyResponse = stats.counter("total_empty_response")
|
||||||
|
|
||||||
|
private def buildExploreRankerRequest(
|
||||||
|
target: Target,
|
||||||
|
countryCode: Option[String],
|
||||||
|
language: Option[String],
|
||||||
|
): ExploreRankerRequest = {
|
||||||
|
ExploreRankerRequest(
|
||||||
|
clientContext = ClientContext(
|
||||||
|
userId = Some(target.targetId),
|
||||||
|
countryCode = countryCode,
|
||||||
|
languageCode = language,
|
||||||
|
),
|
||||||
|
product = Product.NotificationsVideoRecs,
|
||||||
|
productContext = Some(ProductContext.NotificationsVideoRecs(NotificationsVideoRecs())),
|
||||||
|
maxResults = Some(target.params(PushFeatureSwitchParams.MaxExploreVideoTweets))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def get(target: Target): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
target.countryCode,
|
||||||
|
target.inferredUserDeviceLanguage
|
||||||
|
).flatMap {
|
||||||
|
case (countryCode, language) =>
|
||||||
|
val request = buildExploreRankerRequest(target, countryCode, language)
|
||||||
|
exploreRankerStore.get(request).flatMap {
|
||||||
|
case Some(response) =>
|
||||||
|
val exploreResonseTweetIds = response match {
|
||||||
|
case ExploreRankerResponse(ExploreRankerProductResponse
|
||||||
|
.ImmersiveRecsResponse(ImmersiveRecsResponse(immersiveRecsResult))) =>
|
||||||
|
immersiveRecsResult.collect {
|
||||||
|
case ImmersiveRecsResult(ExploreRecommendation
|
||||||
|
.ExploreTweetRecommendation(exploreTweetRecommendation)) =>
|
||||||
|
exploreTweetRecommendation.tweetId
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
Seq.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
totalInputRecs.add(exploreResonseTweetIds.size)
|
||||||
|
totalRequests.incr()
|
||||||
|
AdaptorUtils
|
||||||
|
.getTweetyPieResults(exploreResonseTweetIds.toSet, tweetyPieStore).map {
|
||||||
|
tweetyPieResultMap =>
|
||||||
|
val candidates = tweetyPieResultMap.values.flatten
|
||||||
|
.map(buildVideoRawCandidates(target, _))
|
||||||
|
Some(candidates.toSeq)
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
totalEmptyResponse.incr()
|
||||||
|
Future.None
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
Future.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
PushDeviceUtil.isRecommendationsEligible(target).map { userRecommendationsEligible =>
|
||||||
|
userRecommendationsEligible && target.params(PushFeatureSwitchParams.EnableExploreVideoTweets)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private def buildVideoRawCandidates(
|
||||||
|
target: Target,
|
||||||
|
tweetyPieResult: TweetyPieResult
|
||||||
|
): RawCandidate with OutOfNetworkTweetCandidate = {
|
||||||
|
PushAdaptorUtil.generateOutOfNetworkTweetCandidates(
|
||||||
|
inputTarget = target,
|
||||||
|
id = tweetyPieResult.tweet.id,
|
||||||
|
mediaCRT = MediaCRT(
|
||||||
|
CommonRecommendationType.ExploreVideoTweet,
|
||||||
|
CommonRecommendationType.ExploreVideoTweet,
|
||||||
|
CommonRecommendationType.ExploreVideoTweet
|
||||||
|
),
|
||||||
|
result = Some(tweetyPieResult),
|
||||||
|
localizedEntity = None
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,272 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.cr_mixer.thriftscala.FrsTweetRequest
|
||||||
|
import com.twitter.cr_mixer.thriftscala.NotificationsContext
|
||||||
|
import com.twitter.cr_mixer.thriftscala.Product
|
||||||
|
import com.twitter.cr_mixer.thriftscala.ProductContext
|
||||||
|
import com.twitter.finagle.stats.Counter
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.CandidateSource
|
||||||
|
import com.twitter.frigate.common.base.CandidateSourceEligible
|
||||||
|
import com.twitter.frigate.common.base._
|
||||||
|
import com.twitter.frigate.common.predicate.CommonOutNetworkTweetCandidatesSourcePredicates.filterOutReplyTweet
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.store.CrMixerTweetStore
|
||||||
|
import com.twitter.frigate.pushservice.store.UttEntityHydrationStore
|
||||||
|
import com.twitter.frigate.pushservice.util.MediaCRT
|
||||||
|
import com.twitter.frigate.pushservice.util.PushAdaptorUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.TopicsUtil
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.hermit.constants.AlgorithmFeedbackTokens
|
||||||
|
import com.twitter.hermit.model.Algorithm.Algorithm
|
||||||
|
import com.twitter.hermit.model.Algorithm.CrowdSearchAccounts
|
||||||
|
import com.twitter.hermit.model.Algorithm.ForwardEmailBook
|
||||||
|
import com.twitter.hermit.model.Algorithm.ForwardPhoneBook
|
||||||
|
import com.twitter.hermit.model.Algorithm.ReverseEmailBookIbis
|
||||||
|
import com.twitter.hermit.model.Algorithm.ReversePhoneBook
|
||||||
|
import com.twitter.hermit.store.tweetypie.UserTweet
|
||||||
|
import com.twitter.product_mixer.core.thriftscala.ClientContext
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.tsp.thriftscala.TopicSocialProofRequest
|
||||||
|
import com.twitter.tsp.thriftscala.TopicSocialProofResponse
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
object FRSAlgorithmFeedbackTokenUtil {
|
||||||
|
private val crtsByAlgoToken = Map(
|
||||||
|
getAlgorithmToken(ReverseEmailBookIbis) -> CommonRecommendationType.ReverseAddressbookTweet,
|
||||||
|
getAlgorithmToken(ReversePhoneBook) -> CommonRecommendationType.ReverseAddressbookTweet,
|
||||||
|
getAlgorithmToken(ForwardEmailBook) -> CommonRecommendationType.ForwardAddressbookTweet,
|
||||||
|
getAlgorithmToken(ForwardPhoneBook) -> CommonRecommendationType.ForwardAddressbookTweet,
|
||||||
|
getAlgorithmToken(CrowdSearchAccounts) -> CommonRecommendationType.CrowdSearchTweet
|
||||||
|
)
|
||||||
|
|
||||||
|
def getAlgorithmToken(algorithm: Algorithm): Int = {
|
||||||
|
AlgorithmFeedbackTokens.AlgorithmToFeedbackTokenMap(algorithm)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getCRTForAlgoToken(algorithmToken: Int): Option[CommonRecommendationType] = {
|
||||||
|
crtsByAlgoToken.get(algorithmToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class FRSTweetCandidateAdaptor(
|
||||||
|
crMixerTweetStore: CrMixerTweetStore,
|
||||||
|
tweetyPieStore: ReadableStore[Long, TweetyPieResult],
|
||||||
|
tweetyPieStoreNoVF: ReadableStore[Long, TweetyPieResult],
|
||||||
|
userTweetTweetyPieStore: ReadableStore[UserTweet, TweetyPieResult],
|
||||||
|
uttEntityHydrationStore: UttEntityHydrationStore,
|
||||||
|
topicSocialProofServiceStore: ReadableStore[TopicSocialProofRequest, TopicSocialProofResponse],
|
||||||
|
globalStats: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
|
||||||
|
private val stats = globalStats.scope(this.getClass.getSimpleName)
|
||||||
|
private val crtStats = stats.scope("CandidateDistribution")
|
||||||
|
private val totalRequests = stats.counter("total_requests")
|
||||||
|
|
||||||
|
// Candidate Distribution stats
|
||||||
|
private val reverseAddressbookCounter = crtStats.counter("reverse_addressbook")
|
||||||
|
private val forwardAddressbookCounter = crtStats.counter("forward_addressbook")
|
||||||
|
private val frsTweetCounter = crtStats.counter("frs_tweet")
|
||||||
|
private val nonReplyTweetsCounter = stats.counter("non_reply_tweets")
|
||||||
|
private val crtToCounterMapping: Map[CommonRecommendationType, Counter] = Map(
|
||||||
|
CommonRecommendationType.ReverseAddressbookTweet -> reverseAddressbookCounter,
|
||||||
|
CommonRecommendationType.ForwardAddressbookTweet -> forwardAddressbookCounter,
|
||||||
|
CommonRecommendationType.FrsTweet -> frsTweetCounter
|
||||||
|
)
|
||||||
|
|
||||||
|
private val emptyTweetyPieResult = stats.stat("empty_tweetypie_result")
|
||||||
|
|
||||||
|
private[this] val numberReturnedCandidates = stats.stat("returned_candidates_from_earlybird")
|
||||||
|
private[this] val numberCandidateWithTopic: Counter = stats.counter("num_can_with_topic")
|
||||||
|
private[this] val numberCandidateWithoutTopic: Counter = stats.counter("num_can_without_topic")
|
||||||
|
|
||||||
|
private val userTweetTweetyPieStoreCounter = stats.counter("user_tweet_tweetypie_store")
|
||||||
|
|
||||||
|
override val name: String = this.getClass.getSimpleName
|
||||||
|
|
||||||
|
private def filterInvalidTweets(
|
||||||
|
tweetIds: Seq[Long],
|
||||||
|
target: Target
|
||||||
|
): Future[Map[Long, TweetyPieResult]] = {
|
||||||
|
val resMap = {
|
||||||
|
if (target.params(PushFeatureSwitchParams.EnableF1FromProtectedTweetAuthors)) {
|
||||||
|
userTweetTweetyPieStoreCounter.incr()
|
||||||
|
val keys = tweetIds.map { tweetId =>
|
||||||
|
UserTweet(tweetId, Some(target.targetId))
|
||||||
|
}
|
||||||
|
userTweetTweetyPieStore
|
||||||
|
.multiGet(keys.toSet).map {
|
||||||
|
case (userTweet, resultFut) =>
|
||||||
|
userTweet.tweetId -> resultFut
|
||||||
|
}.toMap
|
||||||
|
} else {
|
||||||
|
(if (target.params(PushFeatureSwitchParams.EnableVFInTweetypie)) {
|
||||||
|
tweetyPieStore
|
||||||
|
} else {
|
||||||
|
tweetyPieStoreNoVF
|
||||||
|
}).multiGet(tweetIds.toSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future.collect(resMap).map { tweetyPieResultMap =>
|
||||||
|
// Filter out replies and generate earlybird candidates only for non-empty tweetypie result
|
||||||
|
val cands = filterOutReplyTweet(tweetyPieResultMap, nonReplyTweetsCounter).collect {
|
||||||
|
case (id: Long, Some(result)) =>
|
||||||
|
id -> result
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyTweetyPieResult.add(tweetyPieResultMap.size - cands.size)
|
||||||
|
cands
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def buildRawCandidates(
|
||||||
|
target: Target,
|
||||||
|
ebCandidates: Seq[FRSTweetCandidate]
|
||||||
|
): Future[Option[Seq[RawCandidate with TweetCandidate]]] = {
|
||||||
|
|
||||||
|
val enableTopic = target.params(PushFeatureSwitchParams.EnableFrsTweetCandidatesTopicAnnotation)
|
||||||
|
val topicScoreThre =
|
||||||
|
target.params(PushFeatureSwitchParams.FrsTweetCandidatesTopicScoreThreshold)
|
||||||
|
|
||||||
|
val ebTweets = ebCandidates.map { ebCandidate =>
|
||||||
|
ebCandidate.tweetId -> ebCandidate.tweetyPieResult
|
||||||
|
}.toMap
|
||||||
|
|
||||||
|
val tweetIdLocalizedEntityMapFut = TopicsUtil.getTweetIdLocalizedEntityMap(
|
||||||
|
target,
|
||||||
|
ebTweets,
|
||||||
|
uttEntityHydrationStore,
|
||||||
|
topicSocialProofServiceStore,
|
||||||
|
enableTopic,
|
||||||
|
topicScoreThre
|
||||||
|
)
|
||||||
|
|
||||||
|
Future.join(target.deviceInfo, tweetIdLocalizedEntityMapFut).map {
|
||||||
|
case (Some(deviceInfo), tweetIdLocalizedEntityMap) =>
|
||||||
|
val candidates = ebCandidates
|
||||||
|
.map { ebCandidate =>
|
||||||
|
val crt = ebCandidate.commonRecType
|
||||||
|
crtToCounterMapping.get(crt).foreach(_.incr())
|
||||||
|
|
||||||
|
val tweetId = ebCandidate.tweetId
|
||||||
|
val localizedEntityOpt = {
|
||||||
|
if (tweetIdLocalizedEntityMap
|
||||||
|
.contains(tweetId) && tweetIdLocalizedEntityMap.contains(
|
||||||
|
tweetId) && deviceInfo.isTopicsEligible) {
|
||||||
|
tweetIdLocalizedEntityMap(tweetId)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PushAdaptorUtil.generateOutOfNetworkTweetCandidates(
|
||||||
|
inputTarget = target,
|
||||||
|
id = ebCandidate.tweetId,
|
||||||
|
mediaCRT = MediaCRT(
|
||||||
|
crt,
|
||||||
|
crt,
|
||||||
|
crt
|
||||||
|
),
|
||||||
|
result = ebCandidate.tweetyPieResult,
|
||||||
|
localizedEntity = localizedEntityOpt)
|
||||||
|
}.filter { candidate =>
|
||||||
|
// If user only has the topic setting enabled, filter out all non-topic cands
|
||||||
|
deviceInfo.isRecommendationsEligible || (deviceInfo.isTopicsEligible && candidate.semanticCoreEntityId.nonEmpty)
|
||||||
|
}
|
||||||
|
|
||||||
|
candidates.map { candidate =>
|
||||||
|
if (candidate.semanticCoreEntityId.nonEmpty) {
|
||||||
|
numberCandidateWithTopic.incr()
|
||||||
|
} else {
|
||||||
|
numberCandidateWithoutTopic.incr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
numberReturnedCandidates.add(candidates.length)
|
||||||
|
Some(candidates)
|
||||||
|
case _ => Some(Seq.empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getTweetCandidatesFromCrMixer(
|
||||||
|
inputTarget: Target,
|
||||||
|
showAllResultsFromFrs: Boolean,
|
||||||
|
): Future[Option[Seq[RawCandidate with TweetCandidate]]] = {
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
inputTarget.seenTweetIds,
|
||||||
|
inputTarget.pushRecItems,
|
||||||
|
inputTarget.countryCode,
|
||||||
|
inputTarget.targetLanguage).flatMap {
|
||||||
|
case (seenTweetIds, pastRecItems, countryCode, language) =>
|
||||||
|
val pastUserRecs = pastRecItems.userIds.toSeq
|
||||||
|
val request = FrsTweetRequest(
|
||||||
|
clientContext = ClientContext(
|
||||||
|
userId = Some(inputTarget.targetId),
|
||||||
|
countryCode = countryCode,
|
||||||
|
languageCode = language
|
||||||
|
),
|
||||||
|
product = Product.Notifications,
|
||||||
|
productContext = Some(ProductContext.NotificationsContext(NotificationsContext())),
|
||||||
|
excludedUserIds = Some(pastUserRecs),
|
||||||
|
excludedTweetIds = Some(seenTweetIds)
|
||||||
|
)
|
||||||
|
crMixerTweetStore.getFRSTweetCandidates(request).flatMap {
|
||||||
|
case Some(response) =>
|
||||||
|
val tweetIds = response.tweets.map(_.tweetId)
|
||||||
|
val validTweets = filterInvalidTweets(tweetIds, inputTarget)
|
||||||
|
validTweets.flatMap { tweetypieMap =>
|
||||||
|
val ebCandidates = response.tweets
|
||||||
|
.map { frsTweet =>
|
||||||
|
val candidateTweetId = frsTweet.tweetId
|
||||||
|
val resultFromTweetyPie = tweetypieMap.get(candidateTweetId)
|
||||||
|
new FRSTweetCandidate {
|
||||||
|
override val tweetId = candidateTweetId
|
||||||
|
override val features = None
|
||||||
|
override val tweetyPieResult = resultFromTweetyPie
|
||||||
|
override val feedbackToken = frsTweet.frsPrimarySource
|
||||||
|
override val commonRecType: CommonRecommendationType = feedbackToken
|
||||||
|
.flatMap(token =>
|
||||||
|
FRSAlgorithmFeedbackTokenUtil.getCRTForAlgoToken(token)).getOrElse(
|
||||||
|
CommonRecommendationType.FrsTweet)
|
||||||
|
}
|
||||||
|
}.filter { ebCandidate =>
|
||||||
|
showAllResultsFromFrs || ebCandidate.commonRecType == CommonRecommendationType.ReverseAddressbookTweet
|
||||||
|
}
|
||||||
|
|
||||||
|
numberReturnedCandidates.add(ebCandidates.length)
|
||||||
|
buildRawCandidates(
|
||||||
|
inputTarget,
|
||||||
|
ebCandidates
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case _ => Future.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def get(inputTarget: Target): Future[Option[Seq[RawCandidate with TweetCandidate]]] = {
|
||||||
|
totalRequests.incr()
|
||||||
|
val enableResultsFromFrs =
|
||||||
|
inputTarget.params(PushFeatureSwitchParams.EnableResultFromFrsCandidates)
|
||||||
|
getTweetCandidatesFromCrMixer(inputTarget, enableResultsFromFrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
lazy val enableFrsCandidates = target.params(PushFeatureSwitchParams.EnableFrsCandidates)
|
||||||
|
PushDeviceUtil.isRecommendationsEligible(target).flatMap { isEnabledForRecosSetting =>
|
||||||
|
PushDeviceUtil.isTopicsEligible(target).map { topicSettingEnabled =>
|
||||||
|
val isEnabledForTopics =
|
||||||
|
topicSettingEnabled && target.params(
|
||||||
|
PushFeatureSwitchParams.EnableFrsTweetCandidatesTopicSetting)
|
||||||
|
(isEnabledForRecosSetting || isEnabledForTopics) && enableFrsCandidates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,107 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base._
|
||||||
|
import com.twitter.frigate.common.candidate._
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.PushParams
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
object GenericCandidates {
|
||||||
|
type Target =
|
||||||
|
TargetUser
|
||||||
|
with UserDetails
|
||||||
|
with TargetDecider
|
||||||
|
with TargetABDecider
|
||||||
|
with TweetImpressionHistory
|
||||||
|
with HTLVisitHistory
|
||||||
|
with MaxTweetAge
|
||||||
|
with NewUserDetails
|
||||||
|
with FrigateHistory
|
||||||
|
with TargetWithSeedUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
case class GenericCandidateAdaptor(
|
||||||
|
genericCandidates: CandidateSource[GenericCandidates.Target, Candidate],
|
||||||
|
tweetyPieStore: ReadableStore[Long, TweetyPieResult],
|
||||||
|
tweetyPieStoreNoVF: ReadableStore[Long, TweetyPieResult],
|
||||||
|
stats: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
|
||||||
|
override val name: String = genericCandidates.name
|
||||||
|
|
||||||
|
private def generateTweetFavCandidate(
|
||||||
|
_target: Target,
|
||||||
|
_tweetId: Long,
|
||||||
|
_socialContextActions: Seq[SocialContextAction],
|
||||||
|
socialContextActionsAllTypes: Seq[SocialContextAction],
|
||||||
|
_tweetyPieResult: Option[TweetyPieResult]
|
||||||
|
): RawCandidate = {
|
||||||
|
new RawCandidate with TweetFavoriteCandidate {
|
||||||
|
override val socialContextActions = _socialContextActions
|
||||||
|
override val socialContextAllTypeActions =
|
||||||
|
socialContextActionsAllTypes
|
||||||
|
val tweetId = _tweetId
|
||||||
|
val target = _target
|
||||||
|
val tweetyPieResult = _tweetyPieResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def generateTweetRetweetCandidate(
|
||||||
|
_target: Target,
|
||||||
|
_tweetId: Long,
|
||||||
|
_socialContextActions: Seq[SocialContextAction],
|
||||||
|
socialContextActionsAllTypes: Seq[SocialContextAction],
|
||||||
|
_tweetyPieResult: Option[TweetyPieResult]
|
||||||
|
): RawCandidate = {
|
||||||
|
new RawCandidate with TweetRetweetCandidate {
|
||||||
|
override val socialContextActions = _socialContextActions
|
||||||
|
override val socialContextAllTypeActions = socialContextActionsAllTypes
|
||||||
|
val tweetId = _tweetId
|
||||||
|
val target = _target
|
||||||
|
val tweetyPieResult = _tweetyPieResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def get(inputTarget: Target): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
genericCandidates.get(inputTarget).map { candidatesOpt =>
|
||||||
|
candidatesOpt
|
||||||
|
.map { candidates =>
|
||||||
|
val candidatesSeq =
|
||||||
|
candidates.collect {
|
||||||
|
case tweetRetweet: TweetRetweetCandidate
|
||||||
|
if inputTarget.params(PushParams.MRTweetRetweetRecsParam) =>
|
||||||
|
generateTweetRetweetCandidate(
|
||||||
|
inputTarget,
|
||||||
|
tweetRetweet.tweetId,
|
||||||
|
tweetRetweet.socialContextActions,
|
||||||
|
tweetRetweet.socialContextAllTypeActions,
|
||||||
|
tweetRetweet.tweetyPieResult)
|
||||||
|
case tweetFavorite: TweetFavoriteCandidate
|
||||||
|
if inputTarget.params(PushParams.MRTweetFavRecsParam) =>
|
||||||
|
generateTweetFavCandidate(
|
||||||
|
inputTarget,
|
||||||
|
tweetFavorite.tweetId,
|
||||||
|
tweetFavorite.socialContextActions,
|
||||||
|
tweetFavorite.socialContextAllTypeActions,
|
||||||
|
tweetFavorite.tweetyPieResult)
|
||||||
|
}
|
||||||
|
candidatesSeq.foreach { candidate =>
|
||||||
|
stats.counter(s"${candidate.commonRecType}_count").incr()
|
||||||
|
}
|
||||||
|
candidatesSeq
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
PushDeviceUtil.isRecommendationsEligible(target).map { isAvailable =>
|
||||||
|
isAvailable && target.params(PushParams.GenericCandidateAdaptorDecider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,280 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.Stat
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.CandidateSource
|
||||||
|
import com.twitter.frigate.common.base.CandidateSourceEligible
|
||||||
|
import com.twitter.frigate.common.store.interests.InterestsLookupRequestWithContext
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.HighQualityCandidateGroupEnum
|
||||||
|
import com.twitter.frigate.pushservice.params.HighQualityCandidateGroupEnum._
|
||||||
|
import com.twitter.frigate.pushservice.params.PushConstants.targetUserAgeFeatureName
|
||||||
|
import com.twitter.frigate.pushservice.params.PushConstants.targetUserPreferredLanguage
|
||||||
|
import com.twitter.frigate.pushservice.params.{PushFeatureSwitchParams => FS}
|
||||||
|
import com.twitter.frigate.pushservice.predicate.TargetPredicates
|
||||||
|
import com.twitter.frigate.pushservice.util.MediaCRT
|
||||||
|
import com.twitter.frigate.pushservice.util.PushAdaptorUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.TopicsUtil
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.interests.thriftscala.InterestId.SemanticCore
|
||||||
|
import com.twitter.interests.thriftscala.UserInterests
|
||||||
|
import com.twitter.language.normalization.UserDisplayLanguage
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripDomain
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripTweet
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripTweets
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
object HighQualityTweetsHelper {
|
||||||
|
def getFollowedTopics(
|
||||||
|
target: Target,
|
||||||
|
interestsWithLookupContextStore: ReadableStore[
|
||||||
|
InterestsLookupRequestWithContext,
|
||||||
|
UserInterests
|
||||||
|
],
|
||||||
|
followedTopicsStats: Stat
|
||||||
|
): Future[Seq[Long]] = {
|
||||||
|
TopicsUtil
|
||||||
|
.getTopicsFollowedByUser(target, interestsWithLookupContextStore, followedTopicsStats).map {
|
||||||
|
userInterestsOpt =>
|
||||||
|
val userInterests = userInterestsOpt.getOrElse(Seq.empty)
|
||||||
|
val extractedTopicIds = userInterests.flatMap {
|
||||||
|
_.interestId match {
|
||||||
|
case SemanticCore(semanticCore) => Some(semanticCore.id)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
extractedTopicIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getTripQueries(
|
||||||
|
target: Target,
|
||||||
|
enabledGroups: Set[HighQualityCandidateGroupEnum.Value],
|
||||||
|
interestsWithLookupContextStore: ReadableStore[
|
||||||
|
InterestsLookupRequestWithContext,
|
||||||
|
UserInterests
|
||||||
|
],
|
||||||
|
sourceIds: Seq[String],
|
||||||
|
stat: Stat
|
||||||
|
): Future[Set[TripDomain]] = {
|
||||||
|
|
||||||
|
val followedTopicIdsSetFut: Future[Set[Long]] = if (enabledGroups.contains(Topic)) {
|
||||||
|
getFollowedTopics(target, interestsWithLookupContextStore, stat).map(topicIds =>
|
||||||
|
topicIds.toSet)
|
||||||
|
} else {
|
||||||
|
Future.value(Set.empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
Future
|
||||||
|
.join(target.featureMap, target.inferredUserDeviceLanguage, followedTopicIdsSetFut).map {
|
||||||
|
case (
|
||||||
|
featureMap,
|
||||||
|
deviceLanguageOpt,
|
||||||
|
followedTopicIds
|
||||||
|
) =>
|
||||||
|
val ageBucketOpt = if (enabledGroups.contains(AgeBucket)) {
|
||||||
|
featureMap.categoricalFeatures.get(targetUserAgeFeatureName)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
val languageOptions: Set[Option[String]] = if (enabledGroups.contains(Language)) {
|
||||||
|
val userPreferredLanguages = featureMap.sparseBinaryFeatures
|
||||||
|
.getOrElse(targetUserPreferredLanguage, Set.empty[String])
|
||||||
|
if (userPreferredLanguages.nonEmpty) {
|
||||||
|
userPreferredLanguages.map(lang => Some(UserDisplayLanguage.toTweetLanguage(lang)))
|
||||||
|
} else {
|
||||||
|
Set(deviceLanguageOpt.map(UserDisplayLanguage.toTweetLanguage))
|
||||||
|
}
|
||||||
|
} else Set(None)
|
||||||
|
|
||||||
|
val followedTopicOptions: Set[Option[Long]] = if (followedTopicIds.nonEmpty) {
|
||||||
|
followedTopicIds.map(topic => Some(topic))
|
||||||
|
} else Set(None)
|
||||||
|
|
||||||
|
val tripQueries = followedTopicOptions.flatMap { topicOption =>
|
||||||
|
languageOptions.flatMap { languageOption =>
|
||||||
|
sourceIds.map { sourceId =>
|
||||||
|
TripDomain(
|
||||||
|
sourceId = sourceId,
|
||||||
|
language = languageOption,
|
||||||
|
placeId = None,
|
||||||
|
topicId = topicOption,
|
||||||
|
gender = None,
|
||||||
|
ageBucket = ageBucketOpt
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tripQueries
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class HighQualityTweetsAdaptor(
|
||||||
|
tripTweetCandidateStore: ReadableStore[TripDomain, TripTweets],
|
||||||
|
interestsWithLookupContextStore: ReadableStore[InterestsLookupRequestWithContext, UserInterests],
|
||||||
|
tweetyPieStore: ReadableStore[Long, TweetyPieResult],
|
||||||
|
tweetyPieStoreNoVF: ReadableStore[Long, TweetyPieResult],
|
||||||
|
globalStats: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
|
||||||
|
override def name: String = this.getClass.getSimpleName
|
||||||
|
|
||||||
|
private val stats = globalStats.scope("HighQualityCandidateAdaptor")
|
||||||
|
private val followedTopicsStats = stats.stat("followed_topics")
|
||||||
|
private val missingResponseCounter = stats.counter("missing_respond_counter")
|
||||||
|
private val crtFatigueCounter = stats.counter("fatigue_by_crt")
|
||||||
|
private val fallbackRequestsCounter = stats.counter("fallback_requests")
|
||||||
|
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
PushDeviceUtil.isRecommendationsEligible(target).map {
|
||||||
|
_ && target.params(FS.HighQualityCandidatesEnableCandidateSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val highQualityCandidateFrequencyPredicate = {
|
||||||
|
TargetPredicates
|
||||||
|
.pushRecTypeFatiguePredicate(
|
||||||
|
CommonRecommendationType.TripHqTweet,
|
||||||
|
FS.HighQualityTweetsPushInterval,
|
||||||
|
FS.MaxHighQualityTweetsPushGivenInterval,
|
||||||
|
stats
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getTripCandidatesStrato(
|
||||||
|
target: Target
|
||||||
|
): Future[Map[Long, Set[TripDomain]]] = {
|
||||||
|
val tripQueriesF: Future[Set[TripDomain]] = HighQualityTweetsHelper.getTripQueries(
|
||||||
|
target = target,
|
||||||
|
enabledGroups = target.params(FS.HighQualityCandidatesEnableGroups).toSet,
|
||||||
|
interestsWithLookupContextStore = interestsWithLookupContextStore,
|
||||||
|
sourceIds = target.params(FS.TripTweetCandidateSourceIds),
|
||||||
|
stat = followedTopicsStats
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val fallbackTripQueriesFut: Future[Set[TripDomain]] =
|
||||||
|
if (target.params(FS.HighQualityCandidatesEnableFallback))
|
||||||
|
HighQualityTweetsHelper.getTripQueries(
|
||||||
|
target = target,
|
||||||
|
enabledGroups = target.params(FS.HighQualityCandidatesFallbackEnabledGroups).toSet,
|
||||||
|
interestsWithLookupContextStore = interestsWithLookupContextStore,
|
||||||
|
sourceIds = target.params(FS.HighQualityCandidatesFallbackSourceIds),
|
||||||
|
stat = followedTopicsStats
|
||||||
|
)
|
||||||
|
else Future.value(Set.empty)
|
||||||
|
|
||||||
|
val initialTweetsFut: Future[Map[TripDomain, Seq[TripTweet]]] = tripQueriesF.flatMap {
|
||||||
|
tripQueries => getTripTweetsByDomains(tripQueries)
|
||||||
|
}
|
||||||
|
|
||||||
|
val tweetsByDomainFut: Future[Map[TripDomain, Seq[TripTweet]]] =
|
||||||
|
if (target.params(FS.HighQualityCandidatesEnableFallback)) {
|
||||||
|
initialTweetsFut.flatMap { candidates =>
|
||||||
|
val minCandidatesForFallback: Int =
|
||||||
|
target.params(FS.HighQualityCandidatesMinNumOfCandidatesToFallback)
|
||||||
|
val validCandidates = candidates.filter(_._2.size >= minCandidatesForFallback)
|
||||||
|
|
||||||
|
if (validCandidates.nonEmpty) {
|
||||||
|
Future.value(validCandidates)
|
||||||
|
} else {
|
||||||
|
fallbackTripQueriesFut.flatMap { fallbackTripDomains =>
|
||||||
|
fallbackRequestsCounter.incr(fallbackTripDomains.size)
|
||||||
|
getTripTweetsByDomains(fallbackTripDomains)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
initialTweetsFut
|
||||||
|
}
|
||||||
|
|
||||||
|
val numOfCandidates: Int = target.params(FS.HighQualityCandidatesNumberOfCandidates)
|
||||||
|
tweetsByDomainFut.map(tweetsByDomain => reformatDomainTweetMap(tweetsByDomain, numOfCandidates))
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getTripTweetsByDomains(
|
||||||
|
tripQueries: Set[TripDomain]
|
||||||
|
): Future[Map[TripDomain, Seq[TripTweet]]] = {
|
||||||
|
Future.collect(tripTweetCandidateStore.multiGet(tripQueries)).map { response =>
|
||||||
|
response
|
||||||
|
.filter(p => p._2.exists(_.tweets.nonEmpty))
|
||||||
|
.mapValues(_.map(_.tweets).getOrElse(Seq.empty))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def reformatDomainTweetMap(
|
||||||
|
tweetsByDomain: Map[TripDomain, Seq[TripTweet]],
|
||||||
|
numOfCandidates: Int
|
||||||
|
): Map[Long, Set[TripDomain]] = tweetsByDomain
|
||||||
|
.flatMap {
|
||||||
|
case (tripDomain, tripTweets) =>
|
||||||
|
tripTweets
|
||||||
|
.sortBy(_.score)(Ordering[Double].reverse)
|
||||||
|
.take(numOfCandidates)
|
||||||
|
.map { tweet => (tweet.tweetId, tripDomain) }
|
||||||
|
}.groupBy(_._1).mapValues(_.map(_._2).toSet)
|
||||||
|
|
||||||
|
private def buildRawCandidate(
|
||||||
|
target: Target,
|
||||||
|
tweetyPieResult: TweetyPieResult,
|
||||||
|
tripDomain: Option[scala.collection.Set[TripDomain]]
|
||||||
|
): RawCandidate = {
|
||||||
|
PushAdaptorUtil.generateOutOfNetworkTweetCandidates(
|
||||||
|
inputTarget = target,
|
||||||
|
id = tweetyPieResult.tweet.id,
|
||||||
|
mediaCRT = MediaCRT(
|
||||||
|
CommonRecommendationType.TripHqTweet,
|
||||||
|
CommonRecommendationType.TripHqTweet,
|
||||||
|
CommonRecommendationType.TripHqTweet
|
||||||
|
),
|
||||||
|
result = Some(tweetyPieResult),
|
||||||
|
tripTweetDomain = tripDomain
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getTweetyPieResults(
|
||||||
|
target: Target,
|
||||||
|
tweetToTripDomain: Map[Long, Set[TripDomain]]
|
||||||
|
): Future[Map[Long, Option[TweetyPieResult]]] = {
|
||||||
|
Future.collect((if (target.params(FS.EnableVFInTweetypie)) {
|
||||||
|
tweetyPieStore
|
||||||
|
} else {
|
||||||
|
tweetyPieStoreNoVF
|
||||||
|
}).multiGet(tweetToTripDomain.keySet))
|
||||||
|
}
|
||||||
|
|
||||||
|
override def get(target: Target): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
for {
|
||||||
|
tweetsToTripDomainMap <- getTripCandidatesStrato(target)
|
||||||
|
tweetyPieResults <- getTweetyPieResults(target, tweetsToTripDomainMap)
|
||||||
|
} yield {
|
||||||
|
val candidates = tweetyPieResults.flatMap {
|
||||||
|
case (tweetId, tweetyPieResultOpt) =>
|
||||||
|
tweetyPieResultOpt.map(buildRawCandidate(target, _, tweetsToTripDomainMap.get(tweetId)))
|
||||||
|
}
|
||||||
|
if (candidates.nonEmpty) {
|
||||||
|
highQualityCandidateFrequencyPredicate(Seq(target))
|
||||||
|
.map(_.head)
|
||||||
|
.map { isTargetFatigueEligible =>
|
||||||
|
if (isTargetFatigueEligible) Some(candidates)
|
||||||
|
else {
|
||||||
|
crtFatigueCounter.incr()
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(candidates.toSeq)
|
||||||
|
} else {
|
||||||
|
missingResponseCounter.incr()
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,152 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.CandidateSource
|
||||||
|
import com.twitter.frigate.common.base.CandidateSourceEligible
|
||||||
|
import com.twitter.frigate.common.base.ListPushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.predicate.TargetPredicates
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.geoduck.service.thriftscala.LocationResponse
|
||||||
|
import com.twitter.interests_discovery.thriftscala.DisplayLocation
|
||||||
|
import com.twitter.interests_discovery.thriftscala.NonPersonalizedRecommendedLists
|
||||||
|
import com.twitter.interests_discovery.thriftscala.RecommendedListsRequest
|
||||||
|
import com.twitter.interests_discovery.thriftscala.RecommendedListsResponse
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
case class ListsToRecommendCandidateAdaptor(
|
||||||
|
listRecommendationsStore: ReadableStore[String, NonPersonalizedRecommendedLists],
|
||||||
|
geoDuckV2Store: ReadableStore[Long, LocationResponse],
|
||||||
|
idsStore: ReadableStore[RecommendedListsRequest, RecommendedListsResponse],
|
||||||
|
globalStats: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
|
||||||
|
override val name: String = this.getClass.getSimpleName
|
||||||
|
|
||||||
|
private[this] val stats = globalStats.scope(name)
|
||||||
|
private[this] val noLocationCodeCounter = stats.counter("no_location_code")
|
||||||
|
private[this] val noCandidatesCounter = stats.counter("no_candidates_for_geo")
|
||||||
|
private[this] val disablePopGeoListsCounter = stats.counter("disable_pop_geo_lists")
|
||||||
|
private[this] val disableIDSListsCounter = stats.counter("disable_ids_lists")
|
||||||
|
|
||||||
|
private def getListCandidate(
|
||||||
|
targetUser: Target,
|
||||||
|
_listId: Long
|
||||||
|
): RawCandidate with ListPushCandidate = {
|
||||||
|
new RawCandidate with ListPushCandidate {
|
||||||
|
override val listId: Long = _listId
|
||||||
|
|
||||||
|
override val commonRecType: CommonRecommendationType = CommonRecommendationType.List
|
||||||
|
|
||||||
|
override val target: Target = targetUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getListsRecommendedFromHistory(
|
||||||
|
target: Target
|
||||||
|
): Future[Seq[Long]] = {
|
||||||
|
target.history.map { history =>
|
||||||
|
history.sortedHistory.flatMap {
|
||||||
|
case (_, notif) if notif.commonRecommendationType == List =>
|
||||||
|
notif.listNotification.map(_.listId)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getIDSListRecs(
|
||||||
|
target: Target,
|
||||||
|
historicalListIds: Seq[Long]
|
||||||
|
): Future[Seq[Long]] = {
|
||||||
|
val request = RecommendedListsRequest(
|
||||||
|
target.targetId,
|
||||||
|
DisplayLocation.ListDiscoveryPage,
|
||||||
|
Some(historicalListIds)
|
||||||
|
)
|
||||||
|
if (target.params(PushFeatureSwitchParams.EnableIDSListRecommendations)) {
|
||||||
|
idsStore.get(request).map {
|
||||||
|
case Some(response) =>
|
||||||
|
response.channels.map(_.id)
|
||||||
|
case _ => Nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disableIDSListsCounter.incr()
|
||||||
|
Future.Nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getPopGeoLists(
|
||||||
|
target: Target,
|
||||||
|
historicalListIds: Seq[Long]
|
||||||
|
): Future[Seq[Long]] = {
|
||||||
|
if (target.params(PushFeatureSwitchParams.EnablePopGeoListRecommendations)) {
|
||||||
|
geoDuckV2Store.get(target.targetId).flatMap {
|
||||||
|
case Some(locationResponse) if locationResponse.geohash.isDefined =>
|
||||||
|
val geoHashLength =
|
||||||
|
target.params(PushFeatureSwitchParams.ListRecommendationsGeoHashLength)
|
||||||
|
val geoHash = locationResponse.geohash.get.take(geoHashLength)
|
||||||
|
listRecommendationsStore
|
||||||
|
.get(s"geohash_$geoHash")
|
||||||
|
.map {
|
||||||
|
case Some(recommendedLists) =>
|
||||||
|
recommendedLists.recommendedListsByAlgo.flatMap { topLists =>
|
||||||
|
topLists.lists.collect {
|
||||||
|
case list if !historicalListIds.contains(list.listId) => list.listId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case _ => Nil
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
noLocationCodeCounter.incr()
|
||||||
|
Future.Nil
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
disablePopGeoListsCounter.incr()
|
||||||
|
Future.Nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def get(target: Target): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
getListsRecommendedFromHistory(target).flatMap { historicalListIds =>
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
getPopGeoLists(target, historicalListIds),
|
||||||
|
getIDSListRecs(target, historicalListIds)
|
||||||
|
)
|
||||||
|
.map {
|
||||||
|
case (popGeoListsIds, idsListIds) =>
|
||||||
|
val candidates = (idsListIds ++ popGeoListsIds).map(getListCandidate(target, _))
|
||||||
|
Some(candidates)
|
||||||
|
case _ =>
|
||||||
|
noCandidatesCounter.incr()
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val pushCapFatiguePredicate = TargetPredicates.pushRecTypeFatiguePredicate(
|
||||||
|
CommonRecommendationType.List,
|
||||||
|
PushFeatureSwitchParams.ListRecommendationsPushInterval,
|
||||||
|
PushFeatureSwitchParams.MaxListRecommendationsPushGivenInterval,
|
||||||
|
stats,
|
||||||
|
)
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
|
||||||
|
val isNotFatigued = pushCapFatiguePredicate.apply(Seq(target)).map(_.head)
|
||||||
|
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
PushDeviceUtil.isRecommendationsEligible(target),
|
||||||
|
isNotFatigued
|
||||||
|
).map {
|
||||||
|
case (userRecommendationsEligible, isUnderCAP) =>
|
||||||
|
userRecommendationsEligible && isUnderCAP && target.params(
|
||||||
|
PushFeatureSwitchParams.EnableListRecommendations)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,54 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.CandidateSource
|
||||||
|
import com.twitter.frigate.common.base.CandidateSourceEligible
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.geoduck.service.thriftscala.LocationResponse
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripDomain
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripTweets
|
||||||
|
import com.twitter.content_mixer.thriftscala.ContentMixerRequest
|
||||||
|
import com.twitter.content_mixer.thriftscala.ContentMixerResponse
|
||||||
|
import com.twitter.geoduck.common.thriftscala.Location
|
||||||
|
import com.twitter.hermit.pop_geo.thriftscala.PopTweetsInPlace
|
||||||
|
import com.twitter.recommendation.interests.discovery.core.model.InterestDomain
|
||||||
|
|
||||||
|
class LoggedOutPushCandidateSourceGenerator(
|
||||||
|
tripTweetCandidateStore: ReadableStore[TripDomain, TripTweets],
|
||||||
|
geoDuckV2Store: ReadableStore[Long, LocationResponse],
|
||||||
|
safeCachedTweetyPieStoreV2: ReadableStore[Long, TweetyPieResult],
|
||||||
|
cachedTweetyPieStoreV2NoVF: ReadableStore[Long, TweetyPieResult],
|
||||||
|
cachedTweetyPieStoreV2: ReadableStore[Long, TweetyPieResult],
|
||||||
|
contentMixerStore: ReadableStore[ContentMixerRequest, ContentMixerResponse],
|
||||||
|
softUserLocationStore: ReadableStore[Long, Location],
|
||||||
|
topTweetsByGeoStore: ReadableStore[InterestDomain[String], Map[String, List[(Long, Double)]]],
|
||||||
|
topTweetsByGeoV2VersionedStore: ReadableStore[String, PopTweetsInPlace],
|
||||||
|
)(
|
||||||
|
implicit val globalStats: StatsReceiver) {
|
||||||
|
val sources: Seq[CandidateSource[Target, RawCandidate] with CandidateSourceEligible[
|
||||||
|
Target,
|
||||||
|
RawCandidate
|
||||||
|
]] = {
|
||||||
|
Seq(
|
||||||
|
TripGeoCandidatesAdaptor(
|
||||||
|
tripTweetCandidateStore,
|
||||||
|
contentMixerStore,
|
||||||
|
safeCachedTweetyPieStoreV2,
|
||||||
|
cachedTweetyPieStoreV2NoVF,
|
||||||
|
globalStats
|
||||||
|
),
|
||||||
|
TopTweetsByGeoAdaptor(
|
||||||
|
geoDuckV2Store,
|
||||||
|
softUserLocationStore,
|
||||||
|
topTweetsByGeoStore,
|
||||||
|
topTweetsByGeoV2VersionedStore,
|
||||||
|
cachedTweetyPieStoreV2,
|
||||||
|
cachedTweetyPieStoreV2NoVF,
|
||||||
|
globalStats
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,101 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.CandidateSource
|
||||||
|
import com.twitter.frigate.common.base.CandidateSourceEligible
|
||||||
|
import com.twitter.frigate.common.base.DiscoverTwitterCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.{PushFeatureSwitchParams => FS}
|
||||||
|
import com.twitter.frigate.pushservice.predicate.DiscoverTwitterPredicate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.TargetPredicates
|
||||||
|
import com.twitter.frigate.pushservice.util.PushAppPermissionUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.frigate.thriftscala.{CommonRecommendationType => CRT}
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class OnboardingPushCandidateAdaptor(
|
||||||
|
globalStats: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
|
||||||
|
override val name: String = this.getClass.getSimpleName
|
||||||
|
|
||||||
|
private[this] val stats = globalStats.scope(name)
|
||||||
|
private[this] val requestNum = stats.counter("request_num")
|
||||||
|
private[this] val addressBookCandNum = stats.counter("address_book_cand_num")
|
||||||
|
private[this] val completeOnboardingCandNum = stats.counter("complete_onboarding_cand_num")
|
||||||
|
|
||||||
|
private def generateOnboardingPushRawCandidate(
|
||||||
|
_target: Target,
|
||||||
|
_commonRecType: CRT
|
||||||
|
): RawCandidate = {
|
||||||
|
new RawCandidate with DiscoverTwitterCandidate {
|
||||||
|
override val target = _target
|
||||||
|
override val commonRecType = _commonRecType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getEligibleCandsForTarget(
|
||||||
|
target: Target
|
||||||
|
): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
val addressBookFatigue =
|
||||||
|
TargetPredicates
|
||||||
|
.pushRecTypeFatiguePredicate(
|
||||||
|
CRT.AddressBookUploadPush,
|
||||||
|
FS.FatigueForOnboardingPushes,
|
||||||
|
FS.MaxOnboardingPushInInterval,
|
||||||
|
stats)(Seq(target)).map(_.head)
|
||||||
|
val completeOnboardingFatigue =
|
||||||
|
TargetPredicates
|
||||||
|
.pushRecTypeFatiguePredicate(
|
||||||
|
CRT.CompleteOnboardingPush,
|
||||||
|
FS.FatigueForOnboardingPushes,
|
||||||
|
FS.MaxOnboardingPushInInterval,
|
||||||
|
stats)(Seq(target)).map(_.head)
|
||||||
|
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
target.appPermissions,
|
||||||
|
addressBookFatigue,
|
||||||
|
completeOnboardingFatigue
|
||||||
|
).map {
|
||||||
|
case (appPermissionOpt, addressBookPredicate, completeOnboardingPredicate) =>
|
||||||
|
val addressBookUploaded =
|
||||||
|
PushAppPermissionUtil.hasTargetUploadedAddressBook(appPermissionOpt)
|
||||||
|
val abUploadCandidate =
|
||||||
|
if (!addressBookUploaded && addressBookPredicate && target.params(
|
||||||
|
FS.EnableAddressBookPush)) {
|
||||||
|
addressBookCandNum.incr()
|
||||||
|
Some(generateOnboardingPushRawCandidate(target, CRT.AddressBookUploadPush))
|
||||||
|
} else if (!addressBookUploaded && (completeOnboardingPredicate ||
|
||||||
|
target.params(FS.DisableOnboardingPushFatigue)) && target.params(
|
||||||
|
FS.EnableCompleteOnboardingPush)) {
|
||||||
|
completeOnboardingCandNum.incr()
|
||||||
|
Some(generateOnboardingPushRawCandidate(target, CRT.CompleteOnboardingPush))
|
||||||
|
} else None
|
||||||
|
|
||||||
|
val allCandidates =
|
||||||
|
Seq(abUploadCandidate).filter(_.isDefined).flatten
|
||||||
|
if (allCandidates.nonEmpty) Some(allCandidates) else None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def get(inputTarget: Target): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
requestNum.incr()
|
||||||
|
val minDurationForMRElapsed =
|
||||||
|
DiscoverTwitterPredicate
|
||||||
|
.minDurationElapsedSinceLastMrPushPredicate(
|
||||||
|
name,
|
||||||
|
FS.MrMinDurationSincePushForOnboardingPushes,
|
||||||
|
stats)(Seq(inputTarget)).map(_.head)
|
||||||
|
minDurationForMRElapsed.flatMap { minDurationElapsed =>
|
||||||
|
if (minDurationElapsed) getEligibleCandsForTarget(inputTarget) else Future.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
PushDeviceUtil
|
||||||
|
.isRecommendationsEligible(target).map(_ && target.params(FS.EnableOnboardingPushes))
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,162 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.content_mixer.thriftscala.ContentMixerRequest
|
||||||
|
import com.twitter.content_mixer.thriftscala.ContentMixerResponse
|
||||||
|
import com.twitter.explore_ranker.thriftscala.ExploreRankerRequest
|
||||||
|
import com.twitter.explore_ranker.thriftscala.ExploreRankerResponse
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base._
|
||||||
|
import com.twitter.frigate.common.candidate._
|
||||||
|
import com.twitter.frigate.common.store.RecentTweetsQuery
|
||||||
|
import com.twitter.frigate.common.store.interests.InterestsLookupRequestWithContext
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.store._
|
||||||
|
import com.twitter.geoduck.common.thriftscala.Location
|
||||||
|
import com.twitter.geoduck.service.thriftscala.LocationResponse
|
||||||
|
import com.twitter.hermit.pop_geo.thriftscala.PopTweetsInPlace
|
||||||
|
import com.twitter.hermit.predicate.socialgraph.RelationEdge
|
||||||
|
import com.twitter.hermit.store.tweetypie.UserTweet
|
||||||
|
import com.twitter.interests.thriftscala.UserInterests
|
||||||
|
import com.twitter.interests_discovery.thriftscala.NonPersonalizedRecommendedLists
|
||||||
|
import com.twitter.interests_discovery.thriftscala.RecommendedListsRequest
|
||||||
|
import com.twitter.interests_discovery.thriftscala.RecommendedListsResponse
|
||||||
|
import com.twitter.recommendation.interests.discovery.core.model.InterestDomain
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripDomain
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripTweets
|
||||||
|
import com.twitter.tsp.thriftscala.TopicSocialProofRequest
|
||||||
|
import com.twitter.tsp.thriftscala.TopicSocialProofResponse
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PushCandidateSourceGenerator generates candidate source list for a given Target user
|
||||||
|
*/
|
||||||
|
class PushCandidateSourceGenerator(
|
||||||
|
earlybirdCandidates: CandidateSource[EarlybirdCandidateSource.Query, EarlybirdCandidate],
|
||||||
|
userTweetEntityGraphCandidates: CandidateSource[UserTweetEntityGraphCandidates.Target, Candidate],
|
||||||
|
cachedTweetyPieStoreV2: ReadableStore[Long, TweetyPieResult],
|
||||||
|
safeCachedTweetyPieStoreV2: ReadableStore[Long, TweetyPieResult],
|
||||||
|
userTweetTweetyPieStore: ReadableStore[UserTweet, TweetyPieResult],
|
||||||
|
safeUserTweetTweetyPieStore: ReadableStore[UserTweet, TweetyPieResult],
|
||||||
|
cachedTweetyPieStoreV2NoVF: ReadableStore[Long, TweetyPieResult],
|
||||||
|
edgeStore: ReadableStore[RelationEdge, Boolean],
|
||||||
|
interestsLookupStore: ReadableStore[InterestsLookupRequestWithContext, UserInterests],
|
||||||
|
uttEntityHydrationStore: UttEntityHydrationStore,
|
||||||
|
geoDuckV2Store: ReadableStore[Long, LocationResponse],
|
||||||
|
topTweetsByGeoStore: ReadableStore[InterestDomain[String], Map[String, List[(Long, Double)]]],
|
||||||
|
topTweetsByGeoV2VersionedStore: ReadableStore[String, PopTweetsInPlace],
|
||||||
|
tweetImpressionsStore: TweetImpressionsStore,
|
||||||
|
recommendedTrendsCandidateSource: RecommendedTrendsCandidateSource,
|
||||||
|
recentTweetsByAuthorStore: ReadableStore[RecentTweetsQuery, Seq[Seq[Long]]],
|
||||||
|
topicSocialProofServiceStore: ReadableStore[TopicSocialProofRequest, TopicSocialProofResponse],
|
||||||
|
crMixerStore: CrMixerTweetStore,
|
||||||
|
contentMixerStore: ReadableStore[ContentMixerRequest, ContentMixerResponse],
|
||||||
|
exploreRankerStore: ReadableStore[ExploreRankerRequest, ExploreRankerResponse],
|
||||||
|
softUserLocationStore: ReadableStore[Long, Location],
|
||||||
|
tripTweetCandidateStore: ReadableStore[TripDomain, TripTweets],
|
||||||
|
listRecsStore: ReadableStore[String, NonPersonalizedRecommendedLists],
|
||||||
|
idsStore: ReadableStore[RecommendedListsRequest, RecommendedListsResponse]
|
||||||
|
)(
|
||||||
|
implicit val globalStats: StatsReceiver) {
|
||||||
|
|
||||||
|
private val earlyBirdFirstDegreeCandidateAdaptor = EarlyBirdFirstDegreeCandidateAdaptor(
|
||||||
|
earlybirdCandidates,
|
||||||
|
cachedTweetyPieStoreV2,
|
||||||
|
cachedTweetyPieStoreV2NoVF,
|
||||||
|
userTweetTweetyPieStore,
|
||||||
|
PushFeatureSwitchParams.NumberOfMaxEarlybirdInNetworkCandidatesParam,
|
||||||
|
globalStats
|
||||||
|
)
|
||||||
|
|
||||||
|
private val frsTweetCandidateAdaptor = FRSTweetCandidateAdaptor(
|
||||||
|
crMixerStore,
|
||||||
|
cachedTweetyPieStoreV2,
|
||||||
|
cachedTweetyPieStoreV2NoVF,
|
||||||
|
userTweetTweetyPieStore,
|
||||||
|
uttEntityHydrationStore,
|
||||||
|
topicSocialProofServiceStore,
|
||||||
|
globalStats
|
||||||
|
)
|
||||||
|
|
||||||
|
private val contentRecommenderMixerAdaptor = ContentRecommenderMixerAdaptor(
|
||||||
|
crMixerStore,
|
||||||
|
safeCachedTweetyPieStoreV2,
|
||||||
|
edgeStore,
|
||||||
|
topicSocialProofServiceStore,
|
||||||
|
uttEntityHydrationStore,
|
||||||
|
globalStats
|
||||||
|
)
|
||||||
|
|
||||||
|
private val tripGeoCandidatesAdaptor = TripGeoCandidatesAdaptor(
|
||||||
|
tripTweetCandidateStore,
|
||||||
|
contentMixerStore,
|
||||||
|
safeCachedTweetyPieStoreV2,
|
||||||
|
cachedTweetyPieStoreV2NoVF,
|
||||||
|
globalStats
|
||||||
|
)
|
||||||
|
|
||||||
|
val sources: Seq[
|
||||||
|
CandidateSource[Target, RawCandidate] with CandidateSourceEligible[
|
||||||
|
Target,
|
||||||
|
RawCandidate
|
||||||
|
]
|
||||||
|
] = {
|
||||||
|
Seq(
|
||||||
|
earlyBirdFirstDegreeCandidateAdaptor,
|
||||||
|
GenericCandidateAdaptor(
|
||||||
|
userTweetEntityGraphCandidates,
|
||||||
|
cachedTweetyPieStoreV2,
|
||||||
|
cachedTweetyPieStoreV2NoVF,
|
||||||
|
globalStats.scope("UserTweetEntityGraphCandidates")
|
||||||
|
),
|
||||||
|
new OnboardingPushCandidateAdaptor(globalStats),
|
||||||
|
TopTweetsByGeoAdaptor(
|
||||||
|
geoDuckV2Store,
|
||||||
|
softUserLocationStore,
|
||||||
|
topTweetsByGeoStore,
|
||||||
|
topTweetsByGeoV2VersionedStore,
|
||||||
|
cachedTweetyPieStoreV2,
|
||||||
|
cachedTweetyPieStoreV2NoVF,
|
||||||
|
globalStats
|
||||||
|
),
|
||||||
|
frsTweetCandidateAdaptor,
|
||||||
|
TopTweetImpressionsCandidateAdaptor(
|
||||||
|
recentTweetsByAuthorStore,
|
||||||
|
cachedTweetyPieStoreV2,
|
||||||
|
cachedTweetyPieStoreV2NoVF,
|
||||||
|
tweetImpressionsStore,
|
||||||
|
globalStats
|
||||||
|
),
|
||||||
|
TrendsCandidatesAdaptor(
|
||||||
|
softUserLocationStore,
|
||||||
|
recommendedTrendsCandidateSource,
|
||||||
|
safeCachedTweetyPieStoreV2,
|
||||||
|
cachedTweetyPieStoreV2NoVF,
|
||||||
|
safeUserTweetTweetyPieStore,
|
||||||
|
globalStats
|
||||||
|
),
|
||||||
|
contentRecommenderMixerAdaptor,
|
||||||
|
tripGeoCandidatesAdaptor,
|
||||||
|
HighQualityTweetsAdaptor(
|
||||||
|
tripTweetCandidateStore,
|
||||||
|
interestsLookupStore,
|
||||||
|
cachedTweetyPieStoreV2,
|
||||||
|
cachedTweetyPieStoreV2NoVF,
|
||||||
|
globalStats
|
||||||
|
),
|
||||||
|
ExploreVideoTweetCandidateAdaptor(
|
||||||
|
exploreRankerStore,
|
||||||
|
cachedTweetyPieStoreV2,
|
||||||
|
globalStats
|
||||||
|
),
|
||||||
|
ListsToRecommendCandidateAdaptor(
|
||||||
|
listRecsStore,
|
||||||
|
geoDuckV2Store,
|
||||||
|
idsStore,
|
||||||
|
globalStats
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,326 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.conversions.DurationOps._
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.CandidateSource
|
||||||
|
import com.twitter.frigate.common.base.CandidateSourceEligible
|
||||||
|
import com.twitter.frigate.common.base.TopTweetImpressionsCandidate
|
||||||
|
import com.twitter.frigate.common.store.RecentTweetsQuery
|
||||||
|
import com.twitter.frigate.common.util.SnowflakeUtils
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.{PushFeatureSwitchParams => FS}
|
||||||
|
import com.twitter.frigate.pushservice.store.TweetImpressionsStore
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.FutureOps
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
case class TweetImpressionsCandidate(
|
||||||
|
tweetId: Long,
|
||||||
|
tweetyPieResultOpt: Option[TweetyPieResult],
|
||||||
|
impressionsCountOpt: Option[Long])
|
||||||
|
|
||||||
|
case class TopTweetImpressionsCandidateAdaptor(
|
||||||
|
recentTweetsFromTflockStore: ReadableStore[RecentTweetsQuery, Seq[Seq[Long]]],
|
||||||
|
tweetyPieStore: ReadableStore[Long, TweetyPieResult],
|
||||||
|
tweetyPieStoreNoVF: ReadableStore[Long, TweetyPieResult],
|
||||||
|
tweetImpressionsStore: TweetImpressionsStore,
|
||||||
|
globalStats: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
|
||||||
|
private val stats = globalStats.scope("TopTweetImpressionsAdaptor")
|
||||||
|
private val tweetImpressionsCandsStat = stats.stat("top_tweet_impressions_cands_dist")
|
||||||
|
|
||||||
|
private val eligibleUsersCounter = stats.counter("eligible_users")
|
||||||
|
private val noneligibleUsersCounter = stats.counter("noneligible_users")
|
||||||
|
private val meetsMinTweetsRequiredCounter = stats.counter("meets_min_tweets_required")
|
||||||
|
private val belowMinTweetsRequiredCounter = stats.counter("below_min_tweets_required")
|
||||||
|
private val aboveMaxInboundFavoritesCounter = stats.counter("above_max_inbound_favorites")
|
||||||
|
private val meetsImpressionsRequiredCounter = stats.counter("meets_impressions_required")
|
||||||
|
private val belowImpressionsRequiredCounter = stats.counter("below_impressions_required")
|
||||||
|
private val meetsFavoritesThresholdCounter = stats.counter("meets_favorites_threshold")
|
||||||
|
private val aboveFavoritesThresholdCounter = stats.counter("above_favorites_threshold")
|
||||||
|
private val emptyImpressionsMapCounter = stats.counter("empty_impressions_map")
|
||||||
|
|
||||||
|
private val tflockResultsStat = stats.stat("tflock", "results")
|
||||||
|
private val emptyTflockResult = stats.counter("tflock", "empty_result")
|
||||||
|
private val nonEmptyTflockResult = stats.counter("tflock", "non_empty_result")
|
||||||
|
|
||||||
|
private val originalTweetsStat = stats.stat("tweets", "original_tweets")
|
||||||
|
private val retweetsStat = stats.stat("tweets", "retweets")
|
||||||
|
private val allRetweetsOnlyCounter = stats.counter("tweets", "all_retweets_only")
|
||||||
|
private val allOriginalTweetsOnlyCounter = stats.counter("tweets", "all_original_tweets_only")
|
||||||
|
|
||||||
|
private val emptyTweetypieMap = stats.counter("", "empty_tweetypie_map")
|
||||||
|
private val emptyTweetyPieResult = stats.stat("", "empty_tweetypie_result")
|
||||||
|
private val allEmptyTweetypieResults = stats.counter("", "all_empty_tweetypie_results")
|
||||||
|
|
||||||
|
private val eligibleUsersAfterImpressionsFilter =
|
||||||
|
stats.counter("eligible_users_after_impressions_filter")
|
||||||
|
private val eligibleUsersAfterFavoritesFilter =
|
||||||
|
stats.counter("eligible_users_after_favorites_filter")
|
||||||
|
private val eligibleUsersWithEligibleTweets =
|
||||||
|
stats.counter("eligible_users_with_eligible_tweets")
|
||||||
|
|
||||||
|
private val eligibleTweetCands = stats.stat("eligible_tweet_cands")
|
||||||
|
private val getCandsRequestCounter =
|
||||||
|
stats.counter("top_tweet_impressions_get_request")
|
||||||
|
|
||||||
|
override val name: String = this.getClass.getSimpleName
|
||||||
|
|
||||||
|
override def get(inputTarget: Target): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
getCandsRequestCounter.incr()
|
||||||
|
val eligibleCandidatesFut = getTweetImpressionsCandidates(inputTarget)
|
||||||
|
eligibleCandidatesFut.map { eligibleCandidates =>
|
||||||
|
if (eligibleCandidates.nonEmpty) {
|
||||||
|
eligibleUsersWithEligibleTweets.incr()
|
||||||
|
eligibleTweetCands.add(eligibleCandidates.size)
|
||||||
|
val candidate = getMostImpressionsTweet(eligibleCandidates)
|
||||||
|
Some(
|
||||||
|
Seq(
|
||||||
|
generateTopTweetImpressionsCandidate(
|
||||||
|
inputTarget,
|
||||||
|
candidate.tweetId,
|
||||||
|
candidate.tweetyPieResultOpt,
|
||||||
|
candidate.impressionsCountOpt.getOrElse(0L))))
|
||||||
|
} else None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getTweetImpressionsCandidates(
|
||||||
|
inputTarget: Target
|
||||||
|
): Future[Seq[TweetImpressionsCandidate]] = {
|
||||||
|
val originalTweets = getRecentOriginalTweetsForUser(inputTarget)
|
||||||
|
originalTweets.flatMap { tweetyPieResultsMap =>
|
||||||
|
val numDaysSearchForOriginalTweets =
|
||||||
|
inputTarget.params(FS.TopTweetImpressionsOriginalTweetsNumDaysSearch)
|
||||||
|
val moreRecentTweetIds =
|
||||||
|
getMoreRecentTweetIds(tweetyPieResultsMap.keySet.toSeq, numDaysSearchForOriginalTweets)
|
||||||
|
val isEligible = isEligibleUser(inputTarget, tweetyPieResultsMap, moreRecentTweetIds)
|
||||||
|
if (isEligible) filterByEligibility(inputTarget, tweetyPieResultsMap, moreRecentTweetIds)
|
||||||
|
else Future.Nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getRecentOriginalTweetsForUser(
|
||||||
|
targetUser: Target
|
||||||
|
): Future[Map[Long, TweetyPieResult]] = {
|
||||||
|
val tweetyPieResultsMapFut = getTflockStoreResults(targetUser).flatMap { recentTweetIds =>
|
||||||
|
FutureOps.mapCollect((targetUser.params(FS.EnableVFInTweetypie) match {
|
||||||
|
case true => tweetyPieStore
|
||||||
|
case false => tweetyPieStoreNoVF
|
||||||
|
}).multiGet(recentTweetIds.toSet))
|
||||||
|
}
|
||||||
|
tweetyPieResultsMapFut.map { tweetyPieResultsMap =>
|
||||||
|
if (tweetyPieResultsMap.isEmpty) {
|
||||||
|
emptyTweetypieMap.incr()
|
||||||
|
Map.empty
|
||||||
|
} else removeRetweets(tweetyPieResultsMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getTflockStoreResults(targetUser: Target): Future[Seq[Long]] = {
|
||||||
|
val maxResults = targetUser.params(FS.TopTweetImpressionsRecentTweetsByAuthorStoreMaxResults)
|
||||||
|
val maxAge = targetUser.params(FS.TopTweetImpressionsTotalFavoritesLimitNumDaysSearch)
|
||||||
|
val recentTweetsQuery =
|
||||||
|
RecentTweetsQuery(
|
||||||
|
userIds = Seq(targetUser.targetId),
|
||||||
|
maxResults = maxResults,
|
||||||
|
maxAge = maxAge.days
|
||||||
|
)
|
||||||
|
recentTweetsFromTflockStore
|
||||||
|
.get(recentTweetsQuery).map {
|
||||||
|
case Some(tweetIdsAll) =>
|
||||||
|
val tweetIds = tweetIdsAll.headOption.getOrElse(Seq.empty)
|
||||||
|
val numTweets = tweetIds.size
|
||||||
|
if (numTweets > 0) {
|
||||||
|
tflockResultsStat.add(numTweets)
|
||||||
|
nonEmptyTflockResult.incr()
|
||||||
|
} else emptyTflockResult.incr()
|
||||||
|
tweetIds
|
||||||
|
case _ => Nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def removeRetweets(
|
||||||
|
tweetyPieResultsMap: Map[Long, Option[TweetyPieResult]]
|
||||||
|
): Map[Long, TweetyPieResult] = {
|
||||||
|
val nonEmptyTweetyPieResults: Map[Long, TweetyPieResult] = tweetyPieResultsMap.collect {
|
||||||
|
case (key, Some(value)) => (key, value)
|
||||||
|
}
|
||||||
|
emptyTweetyPieResult.add(tweetyPieResultsMap.size - nonEmptyTweetyPieResults.size)
|
||||||
|
|
||||||
|
if (nonEmptyTweetyPieResults.nonEmpty) {
|
||||||
|
val originalTweets = nonEmptyTweetyPieResults.filter {
|
||||||
|
case (_, tweetyPieResult) =>
|
||||||
|
tweetyPieResult.sourceTweet.isEmpty
|
||||||
|
}
|
||||||
|
val numOriginalTweets = originalTweets.size
|
||||||
|
val numRetweets = nonEmptyTweetyPieResults.size - originalTweets.size
|
||||||
|
originalTweetsStat.add(numOriginalTweets)
|
||||||
|
retweetsStat.add(numRetweets)
|
||||||
|
if (numRetweets == 0) allOriginalTweetsOnlyCounter.incr()
|
||||||
|
if (numOriginalTweets == 0) allRetweetsOnlyCounter.incr()
|
||||||
|
originalTweets
|
||||||
|
} else {
|
||||||
|
allEmptyTweetypieResults.incr()
|
||||||
|
Map.empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getMoreRecentTweetIds(
|
||||||
|
tweetIds: Seq[Long],
|
||||||
|
numDays: Int
|
||||||
|
): Seq[Long] = {
|
||||||
|
tweetIds.filter { tweetId =>
|
||||||
|
SnowflakeUtils.isRecent(tweetId, numDays.days)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def isEligibleUser(
|
||||||
|
inputTarget: Target,
|
||||||
|
tweetyPieResults: Map[Long, TweetyPieResult],
|
||||||
|
recentTweetIds: Seq[Long]
|
||||||
|
): Boolean = {
|
||||||
|
val minNumTweets = inputTarget.params(FS.TopTweetImpressionsMinNumOriginalTweets)
|
||||||
|
lazy val totalFavoritesLimit =
|
||||||
|
inputTarget.params(FS.TopTweetImpressionsTotalInboundFavoritesLimit)
|
||||||
|
if (recentTweetIds.size >= minNumTweets) {
|
||||||
|
meetsMinTweetsRequiredCounter.incr()
|
||||||
|
val isUnderLimit = isUnderTotalInboundFavoritesLimit(tweetyPieResults, totalFavoritesLimit)
|
||||||
|
if (isUnderLimit) eligibleUsersCounter.incr()
|
||||||
|
else {
|
||||||
|
aboveMaxInboundFavoritesCounter.incr()
|
||||||
|
noneligibleUsersCounter.incr()
|
||||||
|
}
|
||||||
|
isUnderLimit
|
||||||
|
} else {
|
||||||
|
belowMinTweetsRequiredCounter.incr()
|
||||||
|
noneligibleUsersCounter.incr()
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getFavoriteCounts(
|
||||||
|
tweetyPieResult: TweetyPieResult
|
||||||
|
): Long = tweetyPieResult.tweet.counts.flatMap(_.favoriteCount).getOrElse(0L)
|
||||||
|
|
||||||
|
private def isUnderTotalInboundFavoritesLimit(
|
||||||
|
tweetyPieResults: Map[Long, TweetyPieResult],
|
||||||
|
totalFavoritesLimit: Long
|
||||||
|
): Boolean = {
|
||||||
|
val favoritesIterator = tweetyPieResults.valuesIterator.map(getFavoriteCounts)
|
||||||
|
val totalInboundFavorites = favoritesIterator.sum
|
||||||
|
totalInboundFavorites <= totalFavoritesLimit
|
||||||
|
}
|
||||||
|
|
||||||
|
def filterByEligibility(
|
||||||
|
inputTarget: Target,
|
||||||
|
tweetyPieResults: Map[Long, TweetyPieResult],
|
||||||
|
tweetIds: Seq[Long]
|
||||||
|
): Future[Seq[TweetImpressionsCandidate]] = {
|
||||||
|
lazy val minNumImpressions: Long = inputTarget.params(FS.TopTweetImpressionsMinRequired)
|
||||||
|
lazy val maxNumLikes: Long = inputTarget.params(FS.TopTweetImpressionsMaxFavoritesPerTweet)
|
||||||
|
for {
|
||||||
|
filteredImpressionsMap <- getFilteredImpressionsMap(tweetIds, minNumImpressions)
|
||||||
|
tweetIdsFilteredByFavorites <-
|
||||||
|
getTweetIdsFilteredByFavorites(filteredImpressionsMap.keySet, tweetyPieResults, maxNumLikes)
|
||||||
|
} yield {
|
||||||
|
if (filteredImpressionsMap.nonEmpty) eligibleUsersAfterImpressionsFilter.incr()
|
||||||
|
if (tweetIdsFilteredByFavorites.nonEmpty) eligibleUsersAfterFavoritesFilter.incr()
|
||||||
|
|
||||||
|
val candidates = tweetIdsFilteredByFavorites.map { tweetId =>
|
||||||
|
TweetImpressionsCandidate(
|
||||||
|
tweetId,
|
||||||
|
tweetyPieResults.get(tweetId),
|
||||||
|
filteredImpressionsMap.get(tweetId))
|
||||||
|
}
|
||||||
|
tweetImpressionsCandsStat.add(candidates.length)
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getFilteredImpressionsMap(
|
||||||
|
tweetIds: Seq[Long],
|
||||||
|
minNumImpressions: Long
|
||||||
|
): Future[Map[Long, Long]] = {
|
||||||
|
getImpressionsCounts(tweetIds).map { impressionsMap =>
|
||||||
|
if (impressionsMap.isEmpty) emptyImpressionsMapCounter.incr()
|
||||||
|
impressionsMap.filter {
|
||||||
|
case (_, numImpressions) =>
|
||||||
|
val isValid = numImpressions >= minNumImpressions
|
||||||
|
if (isValid) {
|
||||||
|
meetsImpressionsRequiredCounter.incr()
|
||||||
|
} else {
|
||||||
|
belowImpressionsRequiredCounter.incr()
|
||||||
|
}
|
||||||
|
isValid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getTweetIdsFilteredByFavorites(
|
||||||
|
filteredTweetIds: Set[Long],
|
||||||
|
tweetyPieResults: Map[Long, TweetyPieResult],
|
||||||
|
maxNumLikes: Long
|
||||||
|
): Future[Seq[Long]] = {
|
||||||
|
val filteredByFavoritesTweetIds = filteredTweetIds.filter { tweetId =>
|
||||||
|
val tweetyPieResultOpt = tweetyPieResults.get(tweetId)
|
||||||
|
val isValid = tweetyPieResultOpt.exists { tweetyPieResult =>
|
||||||
|
getFavoriteCounts(tweetyPieResult) <= maxNumLikes
|
||||||
|
}
|
||||||
|
if (isValid) meetsFavoritesThresholdCounter.incr()
|
||||||
|
else aboveFavoritesThresholdCounter.incr()
|
||||||
|
isValid
|
||||||
|
}
|
||||||
|
Future(filteredByFavoritesTweetIds.toSeq)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getMostImpressionsTweet(
|
||||||
|
filteredResults: Seq[TweetImpressionsCandidate]
|
||||||
|
): TweetImpressionsCandidate = {
|
||||||
|
val maxImpressions: Long = filteredResults.map {
|
||||||
|
_.impressionsCountOpt.getOrElse(0L)
|
||||||
|
}.max
|
||||||
|
|
||||||
|
val mostImpressionsCandidates: Seq[TweetImpressionsCandidate] =
|
||||||
|
filteredResults.filter(_.impressionsCountOpt.getOrElse(0L) == maxImpressions)
|
||||||
|
|
||||||
|
mostImpressionsCandidates.maxBy(_.tweetId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getImpressionsCounts(
|
||||||
|
tweetIds: Seq[Long]
|
||||||
|
): Future[Map[Long, Long]] = {
|
||||||
|
val impressionCountMap = tweetIds.map { tweetId =>
|
||||||
|
tweetId -> tweetImpressionsStore
|
||||||
|
.getCounts(tweetId).map(_.getOrElse(0L))
|
||||||
|
}.toMap
|
||||||
|
Future.collect(impressionCountMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def generateTopTweetImpressionsCandidate(
|
||||||
|
inputTarget: Target,
|
||||||
|
_tweetId: Long,
|
||||||
|
result: Option[TweetyPieResult],
|
||||||
|
_impressionsCount: Long
|
||||||
|
): RawCandidate = {
|
||||||
|
new RawCandidate with TopTweetImpressionsCandidate {
|
||||||
|
override val target: Target = inputTarget
|
||||||
|
override val tweetId: Long = _tweetId
|
||||||
|
override val tweetyPieResult: Option[TweetyPieResult] = result
|
||||||
|
override val impressionsCount: Long = _impressionsCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
val enabledTopTweetImpressionsNotification =
|
||||||
|
target.params(FS.EnableTopTweetImpressionsNotification)
|
||||||
|
|
||||||
|
PushDeviceUtil
|
||||||
|
.isRecommendationsEligible(target).map(_ && enabledTopTweetImpressionsNotification)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,413 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.Counter
|
||||||
|
import com.twitter.finagle.stats.Stat
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.CandidateSource
|
||||||
|
import com.twitter.frigate.common.base.CandidateSourceEligible
|
||||||
|
import com.twitter.frigate.common.base.TweetCandidate
|
||||||
|
import com.twitter.frigate.common.predicate.CommonOutNetworkTweetCandidatesSourcePredicates.filterOutReplyTweet
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes
|
||||||
|
import com.twitter.frigate.pushservice.params.PopGeoTweetVersion
|
||||||
|
import com.twitter.frigate.pushservice.params.PushParams
|
||||||
|
import com.twitter.frigate.pushservice.params.TopTweetsForGeoCombination
|
||||||
|
import com.twitter.frigate.pushservice.params.TopTweetsForGeoRankingFunction
|
||||||
|
import com.twitter.frigate.pushservice.params.{PushFeatureSwitchParams => FS}
|
||||||
|
import com.twitter.frigate.pushservice.predicate.DiscoverTwitterPredicate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.TargetPredicates
|
||||||
|
import com.twitter.frigate.pushservice.util.MediaCRT
|
||||||
|
import com.twitter.frigate.pushservice.util.PushAdaptorUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.geoduck.common.thriftscala.{Location => GeoLocation}
|
||||||
|
import com.twitter.geoduck.service.thriftscala.LocationResponse
|
||||||
|
import com.twitter.gizmoduck.thriftscala.UserType
|
||||||
|
import com.twitter.hermit.pop_geo.thriftscala.PopTweetsInPlace
|
||||||
|
import com.twitter.recommendation.interests.discovery.core.model.InterestDomain
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.FutureOps
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import com.twitter.util.Time
|
||||||
|
import scala.collection.Map
|
||||||
|
|
||||||
|
case class PlaceTweetScore(place: String, tweetId: Long, score: Double) {
|
||||||
|
def toTweetScore: (Long, Double) = (tweetId, score)
|
||||||
|
}
|
||||||
|
case class TopTweetsByGeoAdaptor(
|
||||||
|
geoduckStoreV2: ReadableStore[Long, LocationResponse],
|
||||||
|
softUserGeoLocationStore: ReadableStore[Long, GeoLocation],
|
||||||
|
topTweetsByGeoStore: ReadableStore[InterestDomain[String], Map[String, List[(Long, Double)]]],
|
||||||
|
topTweetsByGeoStoreV2: ReadableStore[String, PopTweetsInPlace],
|
||||||
|
tweetyPieStore: ReadableStore[Long, TweetyPieResult],
|
||||||
|
tweetyPieStoreNoVF: ReadableStore[Long, TweetyPieResult],
|
||||||
|
globalStats: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
|
||||||
|
override def name: String = this.getClass.getSimpleName
|
||||||
|
|
||||||
|
private[this] val stats = globalStats.scope("TopTweetsByGeoAdaptor")
|
||||||
|
private[this] val noGeohashUserCounter: Counter = stats.counter("users_with_no_geohash_counter")
|
||||||
|
private[this] val incomingRequestCounter: Counter = stats.counter("incoming_request_counter")
|
||||||
|
private[this] val incomingLoggedOutRequestCounter: Counter =
|
||||||
|
stats.counter("incoming_logged_out_request_counter")
|
||||||
|
private[this] val loggedOutRawCandidatesCounter =
|
||||||
|
stats.counter("logged_out_raw_candidates_counter")
|
||||||
|
private[this] val emptyLoggedOutRawCandidatesCounter =
|
||||||
|
stats.counter("logged_out_empty_raw_candidates")
|
||||||
|
private[this] val outputTopTweetsByGeoCounter: Stat =
|
||||||
|
stats.stat("output_top_tweets_by_geo_counter")
|
||||||
|
private[this] val loggedOutPopByGeoV2CandidatesCounter: Counter =
|
||||||
|
stats.counter("logged_out_pop_by_geo_candidates")
|
||||||
|
private[this] val dormantUsersSince14DaysCounter: Counter =
|
||||||
|
stats.counter("dormant_user_since_14_days_counter")
|
||||||
|
private[this] val dormantUsersSince30DaysCounter: Counter =
|
||||||
|
stats.counter("dormant_user_since_30_days_counter")
|
||||||
|
private[this] val nonDormantUsersSince14DaysCounter: Counter =
|
||||||
|
stats.counter("non_dormant_user_since_14_days_counter")
|
||||||
|
private[this] val topTweetsByGeoTake100Counter: Counter =
|
||||||
|
stats.counter("top_tweets_by_geo_take_100_counter")
|
||||||
|
private[this] val combinationRequestsCounter =
|
||||||
|
stats.scope("combination_method_request_counter")
|
||||||
|
private[this] val popGeoTweetVersionCounter =
|
||||||
|
stats.scope("popgeo_tweet_version_counter")
|
||||||
|
private[this] val nonReplyTweetsCounter = stats.counter("non_reply_tweets")
|
||||||
|
|
||||||
|
val MaxGeoHashSize = 4
|
||||||
|
|
||||||
|
private def constructKeys(
|
||||||
|
geohash: Option[String],
|
||||||
|
accountCountryCode: Option[String],
|
||||||
|
keyLengths: Seq[Int],
|
||||||
|
version: PopGeoTweetVersion.Value
|
||||||
|
): Set[String] = {
|
||||||
|
val geohashKeys = geohash match {
|
||||||
|
case Some(hash) => keyLengths.map { version + "_geohash_" + hash.take(_) }
|
||||||
|
case _ => Seq.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
val accountCountryCodeKeys =
|
||||||
|
accountCountryCode.toSeq.map(version + "_country_" + _.toUpperCase)
|
||||||
|
(geohashKeys ++ accountCountryCodeKeys).toSet
|
||||||
|
}
|
||||||
|
|
||||||
|
def convertToPlaceTweetScore(
|
||||||
|
popTweetsInPlace: Seq[PopTweetsInPlace]
|
||||||
|
): Seq[PlaceTweetScore] = {
|
||||||
|
popTweetsInPlace.flatMap {
|
||||||
|
case p =>
|
||||||
|
p.popTweets.map {
|
||||||
|
case popTweet => PlaceTweetScore(p.place, popTweet.tweetId, popTweet.score)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def sortGeoHashTweets(
|
||||||
|
placeTweetScores: Seq[PlaceTweetScore],
|
||||||
|
rankingFunction: TopTweetsForGeoRankingFunction.Value
|
||||||
|
): Seq[PlaceTweetScore] = {
|
||||||
|
rankingFunction match {
|
||||||
|
case TopTweetsForGeoRankingFunction.Score =>
|
||||||
|
placeTweetScores.sortBy(_.score)(Ordering[Double].reverse)
|
||||||
|
case TopTweetsForGeoRankingFunction.GeohashLengthAndThenScore =>
|
||||||
|
placeTweetScores
|
||||||
|
.sortBy(row => (row.place.length, row.score))(Ordering[(Int, Double)].reverse)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getResultsForLambdaStore(
|
||||||
|
inputTarget: Target,
|
||||||
|
geohash: Option[String],
|
||||||
|
store: ReadableStore[String, PopTweetsInPlace],
|
||||||
|
topk: Int,
|
||||||
|
version: PopGeoTweetVersion.Value
|
||||||
|
): Future[Seq[(Long, Double)]] = {
|
||||||
|
inputTarget.accountCountryCode.flatMap { countryCode =>
|
||||||
|
val keys = {
|
||||||
|
if (inputTarget.params(FS.EnableCountryCodeBackoffTopTweetsByGeo))
|
||||||
|
constructKeys(geohash, countryCode, inputTarget.params(FS.GeoHashLengthList), version)
|
||||||
|
else
|
||||||
|
constructKeys(geohash, None, inputTarget.params(FS.GeoHashLengthList), version)
|
||||||
|
}
|
||||||
|
FutureOps
|
||||||
|
.mapCollect(store.multiGet(keys)).map {
|
||||||
|
case geohashTweetMap =>
|
||||||
|
val popTweets =
|
||||||
|
geohashTweetMap.values.flatten.toSeq
|
||||||
|
val results = sortGeoHashTweets(
|
||||||
|
convertToPlaceTweetScore(popTweets),
|
||||||
|
inputTarget.params(FS.RankingFunctionForTopTweetsByGeo))
|
||||||
|
.map(_.toTweetScore).take(topk)
|
||||||
|
results
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getPopGeoTweetsForLoggedOutUsers(
|
||||||
|
inputTarget: Target,
|
||||||
|
store: ReadableStore[String, PopTweetsInPlace]
|
||||||
|
): Future[Seq[(Long, Double)]] = {
|
||||||
|
inputTarget.countryCode.flatMap { countryCode =>
|
||||||
|
val keys = constructKeys(None, countryCode, Seq(4), PopGeoTweetVersion.Prod)
|
||||||
|
FutureOps.mapCollect(store.multiGet(keys)).map {
|
||||||
|
case tweetMap =>
|
||||||
|
val tweets = tweetMap.values.flatten.toSeq
|
||||||
|
loggedOutPopByGeoV2CandidatesCounter.incr(tweets.size)
|
||||||
|
val popTweets = sortGeoHashTweets(
|
||||||
|
convertToPlaceTweetScore(tweets),
|
||||||
|
TopTweetsForGeoRankingFunction.Score).map(_.toTweetScore)
|
||||||
|
popTweets
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getRankedTweets(
|
||||||
|
inputTarget: Target,
|
||||||
|
geohash: Option[String]
|
||||||
|
): Future[Seq[(Long, Double)]] = {
|
||||||
|
val MaxTopTweetsByGeoCandidatesToTake =
|
||||||
|
inputTarget.params(FS.MaxTopTweetsByGeoCandidatesToTake)
|
||||||
|
val scoringFn: String = inputTarget.params(FS.ScoringFuncForTopTweetsByGeo)
|
||||||
|
val combinationMethod = inputTarget.params(FS.TopTweetsByGeoCombinationParam)
|
||||||
|
val popGeoTweetVersion = inputTarget.params(FS.PopGeoTweetVersionParam)
|
||||||
|
|
||||||
|
inputTarget.isHeavyUserState.map { isHeavyUser =>
|
||||||
|
stats
|
||||||
|
.scope(combinationMethod.toString).scope(popGeoTweetVersion.toString).scope(
|
||||||
|
"IsHeavyUser_" + isHeavyUser.toString).counter().incr()
|
||||||
|
}
|
||||||
|
combinationRequestsCounter.scope(combinationMethod.toString).counter().incr()
|
||||||
|
popGeoTweetVersionCounter.scope(popGeoTweetVersion.toString).counter().incr()
|
||||||
|
lazy val geoStoreResults = if (geohash.isDefined) {
|
||||||
|
val hash = geohash.get.take(MaxGeoHashSize)
|
||||||
|
topTweetsByGeoStore
|
||||||
|
.get(
|
||||||
|
InterestDomain[String](hash)
|
||||||
|
)
|
||||||
|
.map {
|
||||||
|
case Some(scoringFnToTweetsMapOpt) =>
|
||||||
|
val tweetsWithScore = scoringFnToTweetsMapOpt
|
||||||
|
.getOrElse(scoringFn, List.empty)
|
||||||
|
val sortedResults = sortGeoHashTweets(
|
||||||
|
tweetsWithScore.map {
|
||||||
|
case (tweetId, score) => PlaceTweetScore(hash, tweetId, score)
|
||||||
|
},
|
||||||
|
TopTweetsForGeoRankingFunction.Score
|
||||||
|
).map(_.toTweetScore).take(
|
||||||
|
MaxTopTweetsByGeoCandidatesToTake
|
||||||
|
)
|
||||||
|
sortedResults
|
||||||
|
case _ => Seq.empty
|
||||||
|
}
|
||||||
|
} else Future.value(Seq.empty)
|
||||||
|
lazy val versionPopGeoTweetResults =
|
||||||
|
getResultsForLambdaStore(
|
||||||
|
inputTarget,
|
||||||
|
geohash,
|
||||||
|
topTweetsByGeoStoreV2,
|
||||||
|
MaxTopTweetsByGeoCandidatesToTake,
|
||||||
|
popGeoTweetVersion
|
||||||
|
)
|
||||||
|
combinationMethod match {
|
||||||
|
case TopTweetsForGeoCombination.Default => geoStoreResults
|
||||||
|
case TopTweetsForGeoCombination.AccountsTweetFavAsBackfill =>
|
||||||
|
Future.join(geoStoreResults, versionPopGeoTweetResults).map {
|
||||||
|
case (geoStoreTweets, versionPopGeoTweets) =>
|
||||||
|
(geoStoreTweets ++ versionPopGeoTweets).take(MaxTopTweetsByGeoCandidatesToTake)
|
||||||
|
}
|
||||||
|
case TopTweetsForGeoCombination.AccountsTweetFavIntermixed =>
|
||||||
|
Future.join(geoStoreResults, versionPopGeoTweetResults).map {
|
||||||
|
case (geoStoreTweets, versionPopGeoTweets) =>
|
||||||
|
CandidateSource.interleaveSeqs(Seq(geoStoreTweets, versionPopGeoTweets))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def get(inputTarget: Target): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
if (inputTarget.isLoggedOutUser) {
|
||||||
|
incomingLoggedOutRequestCounter.incr()
|
||||||
|
val rankedTweets = getPopGeoTweetsForLoggedOutUsers(inputTarget, topTweetsByGeoStoreV2)
|
||||||
|
val rawCandidates = {
|
||||||
|
rankedTweets.map { rt =>
|
||||||
|
FutureOps
|
||||||
|
.mapCollect(
|
||||||
|
tweetyPieStore
|
||||||
|
.multiGet(rt.map { case (tweetId, _) => tweetId }.toSet))
|
||||||
|
.map { tweetyPieResultMap =>
|
||||||
|
val results = buildTopTweetsByGeoRawCandidates(
|
||||||
|
inputTarget,
|
||||||
|
None,
|
||||||
|
tweetyPieResultMap
|
||||||
|
)
|
||||||
|
if (results.isEmpty) {
|
||||||
|
emptyLoggedOutRawCandidatesCounter.incr()
|
||||||
|
}
|
||||||
|
loggedOutRawCandidatesCounter.incr(results.size)
|
||||||
|
Some(results)
|
||||||
|
}
|
||||||
|
}.flatten
|
||||||
|
}
|
||||||
|
rawCandidates
|
||||||
|
} else {
|
||||||
|
incomingRequestCounter.incr()
|
||||||
|
getGeoHashForUsers(inputTarget).flatMap { geohash =>
|
||||||
|
if (geohash.isEmpty) noGeohashUserCounter.incr()
|
||||||
|
getRankedTweets(inputTarget, geohash).map { rt =>
|
||||||
|
if (rt.size == 100) {
|
||||||
|
topTweetsByGeoTake100Counter.incr(1)
|
||||||
|
}
|
||||||
|
FutureOps
|
||||||
|
.mapCollect((inputTarget.params(FS.EnableVFInTweetypie) match {
|
||||||
|
case true => tweetyPieStore
|
||||||
|
case false => tweetyPieStoreNoVF
|
||||||
|
}).multiGet(rt.map { case (tweetId, _) => tweetId }.toSet))
|
||||||
|
.map { tweetyPieResultMap =>
|
||||||
|
Some(
|
||||||
|
buildTopTweetsByGeoRawCandidates(
|
||||||
|
inputTarget,
|
||||||
|
None,
|
||||||
|
filterOutReplyTweet(
|
||||||
|
tweetyPieResultMap,
|
||||||
|
nonReplyTweetsCounter
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.flatten
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getGeoHashForUsers(
|
||||||
|
inputTarget: Target
|
||||||
|
): Future[Option[String]] = {
|
||||||
|
|
||||||
|
inputTarget.targetUser.flatMap {
|
||||||
|
case Some(user) =>
|
||||||
|
user.userType match {
|
||||||
|
case UserType.Soft =>
|
||||||
|
softUserGeoLocationStore
|
||||||
|
.get(inputTarget.targetId)
|
||||||
|
.map(_.flatMap(_.geohash.flatMap(_.stringGeohash)))
|
||||||
|
|
||||||
|
case _ =>
|
||||||
|
geoduckStoreV2.get(inputTarget.targetId).map(_.flatMap(_.geohash))
|
||||||
|
}
|
||||||
|
|
||||||
|
case None => Future.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def buildTopTweetsByGeoRawCandidates(
|
||||||
|
target: PushTypes.Target,
|
||||||
|
locationName: Option[String],
|
||||||
|
topTweets: Map[Long, Option[TweetyPieResult]]
|
||||||
|
): Seq[RawCandidate with TweetCandidate] = {
|
||||||
|
val candidates = topTweets.map { tweetIdTweetyPieResultMap =>
|
||||||
|
PushAdaptorUtil.generateOutOfNetworkTweetCandidates(
|
||||||
|
inputTarget = target,
|
||||||
|
id = tweetIdTweetyPieResultMap._1,
|
||||||
|
mediaCRT = MediaCRT(
|
||||||
|
CommonRecommendationType.GeoPopTweet,
|
||||||
|
CommonRecommendationType.GeoPopTweet,
|
||||||
|
CommonRecommendationType.GeoPopTweet
|
||||||
|
),
|
||||||
|
result = tweetIdTweetyPieResultMap._2,
|
||||||
|
localizedEntity = None
|
||||||
|
)
|
||||||
|
}.toSeq
|
||||||
|
outputTopTweetsByGeoCounter.add(candidates.length)
|
||||||
|
candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
private val topTweetsByGeoFrequencyPredicate = {
|
||||||
|
TargetPredicates
|
||||||
|
.pushRecTypeFatiguePredicate(
|
||||||
|
CommonRecommendationType.GeoPopTweet,
|
||||||
|
FS.TopTweetsByGeoPushInterval,
|
||||||
|
FS.MaxTopTweetsByGeoPushGivenInterval,
|
||||||
|
stats
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getAvailabilityForDormantUser(target: Target): Future[Boolean] = {
|
||||||
|
lazy val isDormantUserNotFatigued = topTweetsByGeoFrequencyPredicate(Seq(target)).map(_.head)
|
||||||
|
lazy val enableTopTweetsByGeoForDormantUsers =
|
||||||
|
target.params(FS.EnableTopTweetsByGeoCandidatesForDormantUsers)
|
||||||
|
|
||||||
|
target.lastHTLVisitTimestamp.flatMap {
|
||||||
|
case Some(lastHTLTimestamp) =>
|
||||||
|
val minTimeSinceLastLogin =
|
||||||
|
target.params(FS.MinimumTimeSinceLastLoginForGeoPopTweetPush).ago
|
||||||
|
val timeSinceInactive = target.params(FS.TimeSinceLastLoginForGeoPopTweetPush).ago
|
||||||
|
val lastActiveTimestamp = Time.fromMilliseconds(lastHTLTimestamp)
|
||||||
|
if (lastActiveTimestamp > minTimeSinceLastLogin) {
|
||||||
|
nonDormantUsersSince14DaysCounter.incr()
|
||||||
|
Future.False
|
||||||
|
} else {
|
||||||
|
dormantUsersSince14DaysCounter.incr()
|
||||||
|
isDormantUserNotFatigued.map { isUserNotFatigued =>
|
||||||
|
lastActiveTimestamp < timeSinceInactive &&
|
||||||
|
enableTopTweetsByGeoForDormantUsers &&
|
||||||
|
isUserNotFatigued
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case _ =>
|
||||||
|
dormantUsersSince30DaysCounter.incr()
|
||||||
|
isDormantUserNotFatigued.map { isUserNotFatigued =>
|
||||||
|
enableTopTweetsByGeoForDormantUsers && isUserNotFatigued
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getAvailabilityForPlaybookSetUp(target: Target): Future[Boolean] = {
|
||||||
|
lazy val enableTopTweetsByGeoForNewUsers = target.params(FS.EnableTopTweetsByGeoCandidates)
|
||||||
|
val isTargetEligibleForMrFatigueCheck = target.isAccountAtleastNDaysOld(
|
||||||
|
target.params(FS.MrMinDurationSincePushForTopTweetsByGeoPushes))
|
||||||
|
val isMrFatigueCheckEnabled =
|
||||||
|
target.params(FS.EnableMrMinDurationSinceMrPushFatigue)
|
||||||
|
val applyPredicateForTopTweetsByGeo =
|
||||||
|
if (isMrFatigueCheckEnabled) {
|
||||||
|
if (isTargetEligibleForMrFatigueCheck) {
|
||||||
|
DiscoverTwitterPredicate
|
||||||
|
.minDurationElapsedSinceLastMrPushPredicate(
|
||||||
|
name,
|
||||||
|
FS.MrMinDurationSincePushForTopTweetsByGeoPushes,
|
||||||
|
stats
|
||||||
|
).andThen(
|
||||||
|
topTweetsByGeoFrequencyPredicate
|
||||||
|
)(Seq(target)).map(_.head)
|
||||||
|
} else {
|
||||||
|
Future.False
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
topTweetsByGeoFrequencyPredicate(Seq(target)).map(_.head)
|
||||||
|
}
|
||||||
|
applyPredicateForTopTweetsByGeo.map { predicateResult =>
|
||||||
|
predicateResult && enableTopTweetsByGeoForNewUsers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
if (target.isLoggedOutUser) {
|
||||||
|
Future.True
|
||||||
|
} else {
|
||||||
|
PushDeviceUtil
|
||||||
|
.isRecommendationsEligible(target).map(
|
||||||
|
_ && target.params(PushParams.PopGeoCandidatesDecider)).flatMap { isAvailable =>
|
||||||
|
if (isAvailable) {
|
||||||
|
Future
|
||||||
|
.join(getAvailabilityForDormantUser(target), getAvailabilityForPlaybookSetUp(target))
|
||||||
|
.map {
|
||||||
|
case (isAvailableForDormantUser, isAvailableForPlaybook) =>
|
||||||
|
isAvailableForDormantUser || isAvailableForPlaybook
|
||||||
|
case _ => false
|
||||||
|
}
|
||||||
|
} else Future.False
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,215 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.events.recos.thriftscala.DisplayLocation
|
||||||
|
import com.twitter.events.recos.thriftscala.TrendsContext
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.CandidateSource
|
||||||
|
import com.twitter.frigate.common.base.CandidateSourceEligible
|
||||||
|
import com.twitter.frigate.common.base.TrendTweetCandidate
|
||||||
|
import com.twitter.frigate.common.base.TrendsCandidate
|
||||||
|
import com.twitter.frigate.common.candidate.RecommendedTrendsCandidateSource
|
||||||
|
import com.twitter.frigate.common.candidate.RecommendedTrendsCandidateSource.Query
|
||||||
|
import com.twitter.frigate.common.predicate.CommonOutNetworkTweetCandidatesSourcePredicates.filterOutReplyTweet
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.adaptor.TrendsCandidatesAdaptor._
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.params.PushParams
|
||||||
|
import com.twitter.frigate.pushservice.predicate.TargetPredicates
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.geoduck.common.thriftscala.Location
|
||||||
|
import com.twitter.gizmoduck.thriftscala.UserType
|
||||||
|
import com.twitter.hermit.store.tweetypie.UserTweet
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import scala.collection.Map
|
||||||
|
|
||||||
|
object TrendsCandidatesAdaptor {
|
||||||
|
type TweetId = Long
|
||||||
|
type EventId = Long
|
||||||
|
}
|
||||||
|
|
||||||
|
case class TrendsCandidatesAdaptor(
|
||||||
|
softUserGeoLocationStore: ReadableStore[Long, Location],
|
||||||
|
recommendedTrendsCandidateSource: RecommendedTrendsCandidateSource,
|
||||||
|
tweetyPieStore: ReadableStore[Long, TweetyPieResult],
|
||||||
|
tweetyPieStoreNoVF: ReadableStore[Long, TweetyPieResult],
|
||||||
|
safeUserTweetTweetyPieStore: ReadableStore[UserTweet, TweetyPieResult],
|
||||||
|
statsReceiver: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
override val name = this.getClass.getSimpleName
|
||||||
|
|
||||||
|
private val trendAdaptorStats = statsReceiver.scope("TrendsCandidatesAdaptor")
|
||||||
|
private val trendTweetCandidateNumber = trendAdaptorStats.counter("trend_tweet_candidate")
|
||||||
|
private val nonReplyTweetsCounter = trendAdaptorStats.counter("non_reply_tweets")
|
||||||
|
|
||||||
|
private def getQuery(target: Target): Future[Query] = {
|
||||||
|
def getUserCountryCode(target: Target): Future[Option[String]] = {
|
||||||
|
target.targetUser.flatMap {
|
||||||
|
case Some(user) if user.userType == UserType.Soft =>
|
||||||
|
softUserGeoLocationStore
|
||||||
|
.get(user.id)
|
||||||
|
.map(_.flatMap(_.simpleRgcResult.flatMap(_.countryCodeAlpha2)))
|
||||||
|
|
||||||
|
case _ => target.accountCountryCode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
countryCode <- getUserCountryCode(target)
|
||||||
|
inferredLanguage <- target.inferredUserDeviceLanguage
|
||||||
|
} yield {
|
||||||
|
Query(
|
||||||
|
userId = target.targetId,
|
||||||
|
displayLocation = DisplayLocation.MagicRecs,
|
||||||
|
languageCode = inferredLanguage,
|
||||||
|
countryCode = countryCode,
|
||||||
|
maxResults = target.params(PushFeatureSwitchParams.MaxRecommendedTrendsToQuery)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query candidates only if sent at most [[PushFeatureSwitchParams.MaxTrendTweetNotificationsInDuration]]
|
||||||
|
* trend tweet notifications in [[PushFeatureSwitchParams.TrendTweetNotificationsFatigueDuration]]
|
||||||
|
*/
|
||||||
|
val trendTweetFatiguePredicate = TargetPredicates.pushRecTypeFatiguePredicate(
|
||||||
|
CommonRecommendationType.TrendTweet,
|
||||||
|
PushFeatureSwitchParams.TrendTweetNotificationsFatigueDuration,
|
||||||
|
PushFeatureSwitchParams.MaxTrendTweetNotificationsInDuration,
|
||||||
|
trendAdaptorStats
|
||||||
|
)
|
||||||
|
|
||||||
|
private val recommendedTrendsWithTweetsCandidateSource: CandidateSource[
|
||||||
|
Target,
|
||||||
|
RawCandidate with TrendsCandidate
|
||||||
|
] = recommendedTrendsCandidateSource
|
||||||
|
.convert[Target, TrendsCandidate](
|
||||||
|
getQuery,
|
||||||
|
recommendedTrendsCandidateSource.identityCandidateMapper
|
||||||
|
)
|
||||||
|
.batchMapValues[Target, RawCandidate with TrendsCandidate](
|
||||||
|
trendsCandidatesToTweetCandidates(_, _, getTweetyPieResults))
|
||||||
|
|
||||||
|
private def getTweetyPieResults(
|
||||||
|
tweetIds: Seq[TweetId],
|
||||||
|
target: Target
|
||||||
|
): Future[Map[TweetId, TweetyPieResult]] = {
|
||||||
|
if (target.params(PushFeatureSwitchParams.EnableSafeUserTweetTweetypieStore)) {
|
||||||
|
Future
|
||||||
|
.collect(
|
||||||
|
safeUserTweetTweetyPieStore.multiGet(
|
||||||
|
tweetIds.toSet.map(UserTweet(_, Some(target.targetId))))).map {
|
||||||
|
_.collect {
|
||||||
|
case (userTweet, Some(tweetyPieResult)) => userTweet.tweetId -> tweetyPieResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Future
|
||||||
|
.collect((target.params(PushFeatureSwitchParams.EnableVFInTweetypie) match {
|
||||||
|
case true => tweetyPieStore
|
||||||
|
case false => tweetyPieStoreNoVF
|
||||||
|
}).multiGet(tweetIds.toSet)).map { tweetyPieResultMap =>
|
||||||
|
filterOutReplyTweet(tweetyPieResultMap, nonReplyTweetsCounter).collect {
|
||||||
|
case (tweetId, Some(tweetyPieResult)) => tweetId -> tweetyPieResult
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param _target: [[Target]] object representing notificaion recipient user
|
||||||
|
* @param trendsCandidates: Sequence of [[TrendsCandidate]] returned from ERS
|
||||||
|
* @return: Seq of trends candidates expanded to associated tweets.
|
||||||
|
*/
|
||||||
|
private def trendsCandidatesToTweetCandidates(
|
||||||
|
_target: Target,
|
||||||
|
trendsCandidates: Seq[TrendsCandidate],
|
||||||
|
getTweetyPieResults: (Seq[TweetId], Target) => Future[Map[TweetId, TweetyPieResult]]
|
||||||
|
): Future[Seq[RawCandidate with TrendsCandidate]] = {
|
||||||
|
|
||||||
|
def generateTrendTweetCandidates(
|
||||||
|
trendCandidate: TrendsCandidate,
|
||||||
|
tweetyPieResults: Map[TweetId, TweetyPieResult]
|
||||||
|
) = {
|
||||||
|
val tweetIds = trendCandidate.context.curatedRepresentativeTweets.getOrElse(Seq.empty) ++
|
||||||
|
trendCandidate.context.algoRepresentativeTweets.getOrElse(Seq.empty)
|
||||||
|
|
||||||
|
tweetIds.flatMap { tweetId =>
|
||||||
|
tweetyPieResults.get(tweetId).map { _tweetyPieResult =>
|
||||||
|
new RawCandidate with TrendTweetCandidate {
|
||||||
|
override val trendId: String = trendCandidate.trendId
|
||||||
|
override val trendName: String = trendCandidate.trendName
|
||||||
|
override val landingUrl: String = trendCandidate.landingUrl
|
||||||
|
override val timeBoundedLandingUrl: Option[String] =
|
||||||
|
trendCandidate.timeBoundedLandingUrl
|
||||||
|
override val context: TrendsContext = trendCandidate.context
|
||||||
|
override val tweetyPieResult: Option[TweetyPieResult] = Some(_tweetyPieResult)
|
||||||
|
override val tweetId: TweetId = _tweetyPieResult.tweet.id
|
||||||
|
override val target: Target = _target
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// collect all tweet ids associated with all trends
|
||||||
|
val allTweetIds = trendsCandidates.flatMap { trendsCandidate =>
|
||||||
|
val context = trendsCandidate.context
|
||||||
|
context.curatedRepresentativeTweets.getOrElse(Seq.empty) ++
|
||||||
|
context.algoRepresentativeTweets.getOrElse(Seq.empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
getTweetyPieResults(allTweetIds, _target)
|
||||||
|
.map { tweetIdToTweetyPieResult =>
|
||||||
|
val trendTweetCandidates = trendsCandidates.flatMap { trendCandidate =>
|
||||||
|
val allTrendTweetCandidates = generateTrendTweetCandidates(
|
||||||
|
trendCandidate,
|
||||||
|
tweetIdToTweetyPieResult
|
||||||
|
)
|
||||||
|
|
||||||
|
val (tweetCandidatesFromCuratedTrends, tweetCandidatesFromNonCuratedTrends) =
|
||||||
|
allTrendTweetCandidates.partition(_.isCuratedTrend)
|
||||||
|
|
||||||
|
tweetCandidatesFromCuratedTrends.filter(
|
||||||
|
_.target.params(PushFeatureSwitchParams.EnableCuratedTrendTweets)) ++
|
||||||
|
tweetCandidatesFromNonCuratedTrends.filter(
|
||||||
|
_.target.params(PushFeatureSwitchParams.EnableNonCuratedTrendTweets))
|
||||||
|
}
|
||||||
|
|
||||||
|
trendTweetCandidateNumber.incr(trendTweetCandidates.size)
|
||||||
|
trendTweetCandidates
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param target: [[Target]] user
|
||||||
|
* @return: true if customer is eligible to receive trend tweet notifications
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
PushDeviceUtil
|
||||||
|
.isRecommendationsEligible(target)
|
||||||
|
.map(target.params(PushParams.TrendsCandidateDecider) && _)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def get(target: Target): Future[Option[Seq[RawCandidate with TrendsCandidate]]] = {
|
||||||
|
recommendedTrendsWithTweetsCandidateSource
|
||||||
|
.get(target)
|
||||||
|
.flatMap {
|
||||||
|
case Some(candidates) if candidates.nonEmpty =>
|
||||||
|
trendTweetFatiguePredicate(Seq(target))
|
||||||
|
.map(_.head)
|
||||||
|
.map { isTargetFatigueEligible =>
|
||||||
|
if (isTargetFatigueEligible) Some(candidates)
|
||||||
|
else None
|
||||||
|
}
|
||||||
|
|
||||||
|
case _ => Future.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
package com.twitter.frigate.pushservice.adaptor
|
||||||
|
|
||||||
|
import com.twitter.content_mixer.thriftscala.ContentMixerProductResponse
|
||||||
|
import com.twitter.content_mixer.thriftscala.ContentMixerRequest
|
||||||
|
import com.twitter.content_mixer.thriftscala.ContentMixerResponse
|
||||||
|
import com.twitter.content_mixer.thriftscala.NotificationsTripTweetsProductContext
|
||||||
|
import com.twitter.content_mixer.thriftscala.Product
|
||||||
|
import com.twitter.content_mixer.thriftscala.ProductContext
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.CandidateSource
|
||||||
|
import com.twitter.frigate.common.base.CandidateSourceEligible
|
||||||
|
import com.twitter.frigate.common.predicate.CommonOutNetworkTweetCandidatesSourcePredicates.filterOutReplyTweet
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.params.PushParams
|
||||||
|
import com.twitter.frigate.pushservice.util.MediaCRT
|
||||||
|
import com.twitter.frigate.pushservice.util.PushAdaptorUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.geoduck.util.country.CountryInfo
|
||||||
|
import com.twitter.product_mixer.core.thriftscala.ClientContext
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripDomain
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripTweets
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
case class TripGeoCandidatesAdaptor(
|
||||||
|
tripTweetCandidateStore: ReadableStore[TripDomain, TripTweets],
|
||||||
|
contentMixerStore: ReadableStore[ContentMixerRequest, ContentMixerResponse],
|
||||||
|
tweetyPieStore: ReadableStore[Long, TweetyPieResult],
|
||||||
|
tweetyPieStoreNoVF: ReadableStore[Long, TweetyPieResult],
|
||||||
|
statsReceiver: StatsReceiver)
|
||||||
|
extends CandidateSource[Target, RawCandidate]
|
||||||
|
with CandidateSourceEligible[Target, RawCandidate] {
|
||||||
|
|
||||||
|
override def name: String = this.getClass.getSimpleName
|
||||||
|
|
||||||
|
private val stats = statsReceiver.scope(name.stripSuffix("$"))
|
||||||
|
|
||||||
|
private val contentMixerRequests = stats.counter("getTripCandidatesContentMixerRequests")
|
||||||
|
private val loggedOutTripTweetIds = stats.counter("logged_out_trip_tweet_ids_count")
|
||||||
|
private val loggedOutRawCandidates = stats.counter("logged_out_raw_candidates_count")
|
||||||
|
private val rawCandidates = stats.counter("raw_candidates_count")
|
||||||
|
private val loggedOutEmptyplaceId = stats.counter("logged_out_empty_place_id_count")
|
||||||
|
private val loggedOutPlaceId = stats.counter("logged_out_place_id_count")
|
||||||
|
private val nonReplyTweetsCounter = stats.counter("non_reply_tweets")
|
||||||
|
|
||||||
|
override def isCandidateSourceAvailable(target: Target): Future[Boolean] = {
|
||||||
|
if (target.isLoggedOutUser) {
|
||||||
|
Future.True
|
||||||
|
} else {
|
||||||
|
for {
|
||||||
|
isRecommendationsSettingEnabled <- PushDeviceUtil.isRecommendationsEligible(target)
|
||||||
|
inferredLanguage <- target.inferredUserDeviceLanguage
|
||||||
|
} yield {
|
||||||
|
isRecommendationsSettingEnabled &&
|
||||||
|
inferredLanguage.nonEmpty &&
|
||||||
|
target.params(PushParams.TripGeoTweetCandidatesDecider)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private def buildRawCandidate(target: Target, tweetyPieResult: TweetyPieResult): RawCandidate = {
|
||||||
|
PushAdaptorUtil.generateOutOfNetworkTweetCandidates(
|
||||||
|
inputTarget = target,
|
||||||
|
id = tweetyPieResult.tweet.id,
|
||||||
|
mediaCRT = MediaCRT(
|
||||||
|
CommonRecommendationType.TripGeoTweet,
|
||||||
|
CommonRecommendationType.TripGeoTweet,
|
||||||
|
CommonRecommendationType.TripGeoTweet
|
||||||
|
),
|
||||||
|
result = Some(tweetyPieResult),
|
||||||
|
localizedEntity = None
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override def get(target: Target): Future[Option[Seq[RawCandidate]]] = {
|
||||||
|
if (target.isLoggedOutUser) {
|
||||||
|
for {
|
||||||
|
tripTweetIds <- getTripCandidatesForLoggedOutTarget(target)
|
||||||
|
tweetyPieResults <- Future.collect(tweetyPieStoreNoVF.multiGet(tripTweetIds))
|
||||||
|
} yield {
|
||||||
|
val candidates = tweetyPieResults.values.flatten.map(buildRawCandidate(target, _))
|
||||||
|
if (candidates.nonEmpty) {
|
||||||
|
loggedOutRawCandidates.incr(candidates.size)
|
||||||
|
Some(candidates.toSeq)
|
||||||
|
} else None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for {
|
||||||
|
tripTweetIds <- getTripCandidatesContentMixer(target)
|
||||||
|
tweetyPieResults <-
|
||||||
|
Future.collect((target.params(PushFeatureSwitchParams.EnableVFInTweetypie) match {
|
||||||
|
case true => tweetyPieStore
|
||||||
|
case false => tweetyPieStoreNoVF
|
||||||
|
}).multiGet(tripTweetIds))
|
||||||
|
} yield {
|
||||||
|
val nonReplyTweets = filterOutReplyTweet(tweetyPieResults, nonReplyTweetsCounter)
|
||||||
|
val candidates = nonReplyTweets.values.flatten.map(buildRawCandidate(target, _))
|
||||||
|
if (candidates.nonEmpty && target.params(
|
||||||
|
PushFeatureSwitchParams.TripTweetCandidateReturnEnable)) {
|
||||||
|
rawCandidates.incr(candidates.size)
|
||||||
|
Some(candidates.toSeq)
|
||||||
|
} else None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getTripCandidatesContentMixer(
|
||||||
|
target: Target
|
||||||
|
): Future[Set[Long]] = {
|
||||||
|
contentMixerRequests.incr()
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
target.inferredUserDeviceLanguage,
|
||||||
|
target.deviceInfo
|
||||||
|
)
|
||||||
|
.flatMap {
|
||||||
|
case (languageOpt, deviceInfoOpt) =>
|
||||||
|
contentMixerStore
|
||||||
|
.get(
|
||||||
|
ContentMixerRequest(
|
||||||
|
clientContext = ClientContext(
|
||||||
|
userId = Some(target.targetId),
|
||||||
|
languageCode = languageOpt,
|
||||||
|
userAgent = deviceInfoOpt.flatMap(_.guessedPrimaryDeviceUserAgent.map(_.toString))
|
||||||
|
),
|
||||||
|
product = Product.NotificationsTripTweets,
|
||||||
|
productContext = Some(
|
||||||
|
ProductContext.NotificationsTripTweetsProductContext(
|
||||||
|
NotificationsTripTweetsProductContext()
|
||||||
|
)),
|
||||||
|
cursor = None,
|
||||||
|
maxResults =
|
||||||
|
Some(target.params(PushFeatureSwitchParams.TripTweetMaxTotalCandidates))
|
||||||
|
)
|
||||||
|
).map {
|
||||||
|
_.map { rawResponse =>
|
||||||
|
val tripResponse =
|
||||||
|
rawResponse.contentMixerProductResponse
|
||||||
|
.asInstanceOf[
|
||||||
|
ContentMixerProductResponse.NotificationsTripTweetsProductResponse]
|
||||||
|
.notificationsTripTweetsProductResponse
|
||||||
|
|
||||||
|
tripResponse.results.map(_.tweetResult.tweetId).toSet
|
||||||
|
}.getOrElse(Set.empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getTripCandidatesForLoggedOutTarget(
|
||||||
|
target: Target
|
||||||
|
): Future[Set[Long]] = {
|
||||||
|
Future.join(target.targetLanguage, target.countryCode).flatMap {
|
||||||
|
case (Some(lang), Some(country)) =>
|
||||||
|
val placeId = CountryInfo.lookupByCode(country).map(_.placeIdLong)
|
||||||
|
if (placeId.nonEmpty) {
|
||||||
|
loggedOutPlaceId.incr()
|
||||||
|
} else {
|
||||||
|
loggedOutEmptyplaceId.incr()
|
||||||
|
}
|
||||||
|
val tripSource = "TOP_GEO_V3_LR"
|
||||||
|
val tripQuery = TripDomain(
|
||||||
|
sourceId = tripSource,
|
||||||
|
language = Some(lang),
|
||||||
|
placeId = placeId,
|
||||||
|
topicId = None
|
||||||
|
)
|
||||||
|
val response = tripTweetCandidateStore.get(tripQuery)
|
||||||
|
val tripTweetIds =
|
||||||
|
response.map { res =>
|
||||||
|
if (res.isDefined) {
|
||||||
|
res.get.tweets
|
||||||
|
.sortBy(_.score)(Ordering[Double].reverse).map(_.tweetId).toSet
|
||||||
|
} else {
|
||||||
|
Set.empty[Long]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tripTweetIds.map { ids => loggedOutTripTweetIds.incr(ids.size) }
|
||||||
|
tripTweetIds
|
||||||
|
|
||||||
|
case (_, _) => Future.value(Set.empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,461 @@
|
|||||||
|
package com.twitter.frigate.pushservice.config
|
||||||
|
|
||||||
|
import com.twitter.abdecider.LoggingABDecider
|
||||||
|
import com.twitter.abuse.detection.scoring.thriftscala.TweetScoringRequest
|
||||||
|
import com.twitter.abuse.detection.scoring.thriftscala.TweetScoringResponse
|
||||||
|
import com.twitter.audience_rewards.thriftscala.HasSuperFollowingRelationshipRequest
|
||||||
|
import com.twitter.channels.common.thriftscala.ApiList
|
||||||
|
import com.twitter.datatools.entityservice.entities.sports.thriftscala._
|
||||||
|
import com.twitter.decider.Decider
|
||||||
|
import com.twitter.discovery.common.configapi.ConfigParamsBuilder
|
||||||
|
import com.twitter.escherbird.common.thriftscala.QualifiedId
|
||||||
|
import com.twitter.escherbird.metadata.thriftscala.EntityMegadata
|
||||||
|
import com.twitter.eventbus.client.EventBusPublisher
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.finagle.thrift.ClientId
|
||||||
|
import com.twitter.frigate.common.base._
|
||||||
|
import com.twitter.frigate.common.candidate._
|
||||||
|
import com.twitter.frigate.common.history._
|
||||||
|
import com.twitter.frigate.common.ml.base._
|
||||||
|
import com.twitter.frigate.common.ml.feature._
|
||||||
|
import com.twitter.frigate.common.store._
|
||||||
|
import com.twitter.frigate.common.store.deviceinfo.DeviceInfo
|
||||||
|
import com.twitter.frigate.common.store.interests.InterestsLookupRequestWithContext
|
||||||
|
import com.twitter.frigate.common.store.interests.UserId
|
||||||
|
import com.twitter.frigate.common.util._
|
||||||
|
import com.twitter.frigate.data_pipeline.features_common._
|
||||||
|
import com.twitter.frigate.data_pipeline.thriftscala.UserHistoryKey
|
||||||
|
import com.twitter.frigate.data_pipeline.thriftscala.UserHistoryValue
|
||||||
|
import com.twitter.frigate.dau_model.thriftscala.DauProbability
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.FanoutEvent
|
||||||
|
import com.twitter.frigate.pushcap.thriftscala.PushcapUserHistory
|
||||||
|
import com.twitter.frigate.pushservice.ml._
|
||||||
|
import com.twitter.frigate.pushservice.params.DeciderKey
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitches
|
||||||
|
import com.twitter.frigate.pushservice.params.PushParams
|
||||||
|
import com.twitter.frigate.pushservice.send_handler.SendHandlerPushCandidateHydrator
|
||||||
|
import com.twitter.frigate.pushservice.refresh_handler.PushCandidateHydrator
|
||||||
|
import com.twitter.frigate.pushservice.store._
|
||||||
|
import com.twitter.frigate.pushservice.store.{Ibis2Store => PushIbis2Store}
|
||||||
|
import com.twitter.frigate.pushservice.take.NotificationServiceRequest
|
||||||
|
import com.twitter.frigate.pushservice.thriftscala.PushRequestScribe
|
||||||
|
import com.twitter.frigate.scribe.thriftscala.NotificationScribe
|
||||||
|
import com.twitter.frigate.thriftscala._
|
||||||
|
import com.twitter.frigate.user_states.thriftscala.MRUserHmmState
|
||||||
|
import com.twitter.geoduck.common.thriftscala.{Location => GeoLocation}
|
||||||
|
import com.twitter.geoduck.service.thriftscala.LocationResponse
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.hermit.pop_geo.thriftscala.PopTweetsInPlace
|
||||||
|
import com.twitter.hermit.predicate.socialgraph.RelationEdge
|
||||||
|
import com.twitter.hermit.predicate.tweetypie.Perspective
|
||||||
|
import com.twitter.hermit.predicate.tweetypie.UserTweet
|
||||||
|
import com.twitter.hermit.store.semantic_core.SemanticEntityForQuery
|
||||||
|
import com.twitter.hermit.store.tweetypie.{UserTweet => TweetyPieUserTweet}
|
||||||
|
import com.twitter.hermit.stp.thriftscala.STPResult
|
||||||
|
import com.twitter.hss.api.thriftscala.UserHealthSignalResponse
|
||||||
|
import com.twitter.interests.thriftscala.InterestId
|
||||||
|
import com.twitter.interests.thriftscala.{UserInterests => Interests}
|
||||||
|
import com.twitter.interests_discovery.thriftscala.NonPersonalizedRecommendedLists
|
||||||
|
import com.twitter.interests_discovery.thriftscala.RecommendedListsRequest
|
||||||
|
import com.twitter.interests_discovery.thriftscala.RecommendedListsResponse
|
||||||
|
import com.twitter.livevideo.timeline.domain.v2.{Event => LiveEvent}
|
||||||
|
import com.twitter.ml.api.thriftscala.{DataRecord => ThriftDataRecord}
|
||||||
|
import com.twitter.ml.featurestore.lib.dynamic.DynamicFeatureStoreClient
|
||||||
|
import com.twitter.notificationservice.genericfeedbackstore.FeedbackPromptValue
|
||||||
|
import com.twitter.notificationservice.genericfeedbackstore.GenericFeedbackStore
|
||||||
|
import com.twitter.notificationservice.scribe.manhattan.GenericNotificationsFeedbackRequest
|
||||||
|
import com.twitter.notificationservice.thriftscala.CaretFeedbackDetails
|
||||||
|
import com.twitter.notificationservice.thriftscala.CreateGenericNotificationResponse
|
||||||
|
import com.twitter.nrel.heavyranker.CandidateFeatureHydrator
|
||||||
|
import com.twitter.nrel.heavyranker.{FeatureHydrator => MRFeatureHydrator}
|
||||||
|
import com.twitter.nrel.heavyranker.{TargetFeatureHydrator => RelevanceTargetFeatureHydrator}
|
||||||
|
import com.twitter.onboarding.task.service.thriftscala.FatigueFlowEnrollment
|
||||||
|
import com.twitter.permissions_storage.thriftscala.AppPermission
|
||||||
|
import com.twitter.recommendation.interests.discovery.core.model.InterestDomain
|
||||||
|
import com.twitter.recos.user_tweet_entity_graph.thriftscala.RecommendTweetEntityRequest
|
||||||
|
import com.twitter.recos.user_tweet_entity_graph.thriftscala.RecommendTweetEntityResponse
|
||||||
|
import com.twitter.recos.user_user_graph.thriftscala.RecommendUserRequest
|
||||||
|
import com.twitter.recos.user_user_graph.thriftscala.RecommendUserResponse
|
||||||
|
import com.twitter.rux.common.strato.thriftscala.UserTargetingProperty
|
||||||
|
import com.twitter.scio.nsfw_user_segmentation.thriftscala.NSFWProducer
|
||||||
|
import com.twitter.scio.nsfw_user_segmentation.thriftscala.NSFWUserSegmentation
|
||||||
|
import com.twitter.search.common.features.thriftscala.ThriftSearchResultFeatures
|
||||||
|
import com.twitter.search.earlybird.thriftscala.EarlybirdRequest
|
||||||
|
import com.twitter.search.earlybird.thriftscala.ThriftSearchResult
|
||||||
|
import com.twitter.service.gen.scarecrow.thriftscala.Event
|
||||||
|
import com.twitter.service.gen.scarecrow.thriftscala.TieredActionResult
|
||||||
|
import com.twitter.service.metastore.gen.thriftscala.Location
|
||||||
|
import com.twitter.service.metastore.gen.thriftscala.UserLanguages
|
||||||
|
import com.twitter.servo.decider.DeciderGateBuilder
|
||||||
|
import com.twitter.simclusters_v2.thriftscala.SimClustersInferredEntities
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie.TweetyPieResult
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.strato.columns.frigate.logged_out_web_notifications.thriftscala.LOWebNotificationMetadata
|
||||||
|
import com.twitter.strato.columns.notifications.thriftscala.SourceDestUserRequest
|
||||||
|
import com.twitter.strato.client.{UserId => StratoUserId}
|
||||||
|
import com.twitter.timelines.configapi
|
||||||
|
import com.twitter.timelines.configapi.CompositeConfig
|
||||||
|
import com.twitter.timelinescorer.thriftscala.v1.ScoredTweet
|
||||||
|
import com.twitter.topiclisting.TopicListing
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripDomain
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripTweets
|
||||||
|
import com.twitter.tsp.thriftscala.TopicSocialProofRequest
|
||||||
|
import com.twitter.tsp.thriftscala.TopicSocialProofResponse
|
||||||
|
import com.twitter.ubs.thriftscala.SellerTrack
|
||||||
|
import com.twitter.ubs.thriftscala.AudioSpace
|
||||||
|
import com.twitter.ubs.thriftscala.Participants
|
||||||
|
import com.twitter.ubs.thriftscala.SellerApplicationState
|
||||||
|
import com.twitter.user_session_store.thriftscala.UserSession
|
||||||
|
import com.twitter.util.Duration
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import com.twitter.wtf.scalding.common.thriftscala.UserFeatures
|
||||||
|
|
||||||
|
trait Config {
|
||||||
|
self =>
|
||||||
|
|
||||||
|
def isServiceLocal: Boolean
|
||||||
|
|
||||||
|
def localConfigRepoPath: String
|
||||||
|
|
||||||
|
def inMemCacheOff: Boolean
|
||||||
|
|
||||||
|
def historyStore: PushServiceHistoryStore
|
||||||
|
|
||||||
|
def emailHistoryStore: PushServiceHistoryStore
|
||||||
|
|
||||||
|
def strongTiesStore: ReadableStore[Long, STPResult]
|
||||||
|
|
||||||
|
def safeUserStore: ReadableStore[Long, User]
|
||||||
|
|
||||||
|
def deviceInfoStore: ReadableStore[Long, DeviceInfo]
|
||||||
|
|
||||||
|
def edgeStore: ReadableStore[RelationEdge, Boolean]
|
||||||
|
|
||||||
|
def socialGraphServiceProcessStore: ReadableStore[RelationEdge, Boolean]
|
||||||
|
|
||||||
|
def userUtcOffsetStore: ReadableStore[Long, Duration]
|
||||||
|
|
||||||
|
def cachedTweetyPieStoreV2: ReadableStore[Long, TweetyPieResult]
|
||||||
|
|
||||||
|
def safeCachedTweetyPieStoreV2: ReadableStore[Long, TweetyPieResult]
|
||||||
|
|
||||||
|
def userTweetTweetyPieStore: ReadableStore[TweetyPieUserTweet, TweetyPieResult]
|
||||||
|
|
||||||
|
def safeUserTweetTweetyPieStore: ReadableStore[TweetyPieUserTweet, TweetyPieResult]
|
||||||
|
|
||||||
|
def cachedTweetyPieStoreV2NoVF: ReadableStore[Long, TweetyPieResult]
|
||||||
|
|
||||||
|
def tweetContentFeatureCacheStore: ReadableStore[Long, ThriftDataRecord]
|
||||||
|
|
||||||
|
def scarecrowCheckEventStore: ReadableStore[Event, TieredActionResult]
|
||||||
|
|
||||||
|
def userTweetPerspectiveStore: ReadableStore[UserTweet, Perspective]
|
||||||
|
|
||||||
|
def userCountryStore: ReadableStore[Long, Location]
|
||||||
|
|
||||||
|
def pushInfoStore: ReadableStore[Long, UserForPushTargeting]
|
||||||
|
|
||||||
|
def loggedOutPushInfoStore: ReadableStore[Long, LOWebNotificationMetadata]
|
||||||
|
|
||||||
|
def tweetImpressionStore: ReadableStore[Long, Seq[Long]]
|
||||||
|
|
||||||
|
def audioSpaceStore: ReadableStore[String, AudioSpace]
|
||||||
|
|
||||||
|
def basketballGameScoreStore: ReadableStore[QualifiedId, BasketballGameLiveUpdate]
|
||||||
|
|
||||||
|
def baseballGameScoreStore: ReadableStore[QualifiedId, BaseballGameLiveUpdate]
|
||||||
|
|
||||||
|
def cricketMatchScoreStore: ReadableStore[QualifiedId, CricketMatchLiveUpdate]
|
||||||
|
|
||||||
|
def soccerMatchScoreStore: ReadableStore[QualifiedId, SoccerMatchLiveUpdate]
|
||||||
|
|
||||||
|
def nflGameScoreStore: ReadableStore[QualifiedId, NflFootballGameLiveUpdate]
|
||||||
|
|
||||||
|
def topicSocialProofServiceStore: ReadableStore[TopicSocialProofRequest, TopicSocialProofResponse]
|
||||||
|
|
||||||
|
def spaceDeviceFollowStore: ReadableStore[SourceDestUserRequest, Boolean]
|
||||||
|
|
||||||
|
def audioSpaceParticipantsStore: ReadableStore[String, Participants]
|
||||||
|
|
||||||
|
def notificationServiceSender: ReadableStore[
|
||||||
|
NotificationServiceRequest,
|
||||||
|
CreateGenericNotificationResponse
|
||||||
|
]
|
||||||
|
|
||||||
|
def ocfFatigueStore: ReadableStore[OCFHistoryStoreKey, FatigueFlowEnrollment]
|
||||||
|
|
||||||
|
def dauProbabilityStore: ReadableStore[Long, DauProbability]
|
||||||
|
|
||||||
|
def hydratedLabeledPushRecsStore: ReadableStore[UserHistoryKey, UserHistoryValue]
|
||||||
|
|
||||||
|
def userHTLLastVisitStore: ReadableStore[Long, Seq[Long]]
|
||||||
|
|
||||||
|
def userLanguagesStore: ReadableStore[Long, UserLanguages]
|
||||||
|
|
||||||
|
def topTweetsByGeoStore: ReadableStore[InterestDomain[String], Map[String, List[
|
||||||
|
(Long, Double)
|
||||||
|
]]]
|
||||||
|
|
||||||
|
def topTweetsByGeoV2VersionedStore: ReadableStore[String, PopTweetsInPlace]
|
||||||
|
|
||||||
|
lazy val pushRecItemStore: ReadableStore[PushRecItemsKey, RecItems] = PushRecItemStore(
|
||||||
|
hydratedLabeledPushRecsStore
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val labeledPushRecsVerifyingStore: ReadableStore[
|
||||||
|
LabeledPushRecsVerifyingStoreKey,
|
||||||
|
LabeledPushRecsVerifyingStoreResponse
|
||||||
|
] =
|
||||||
|
LabeledPushRecsVerifyingStore(
|
||||||
|
hydratedLabeledPushRecsStore,
|
||||||
|
historyStore
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val labeledPushRecsDecideredStore: ReadableStore[LabeledPushRecsStoreKey, UserHistoryValue] =
|
||||||
|
LabeledPushRecsDecideredStore(
|
||||||
|
labeledPushRecsVerifyingStore,
|
||||||
|
useHydratedLabeledSendsForFeaturesDeciderKey,
|
||||||
|
verifyHydratedLabeledSendsForFeaturesDeciderKey
|
||||||
|
)
|
||||||
|
|
||||||
|
def onlineUserHistoryStore: ReadableStore[OnlineUserHistoryKey, UserHistoryValue]
|
||||||
|
|
||||||
|
def nsfwConsumerStore: ReadableStore[Long, NSFWUserSegmentation]
|
||||||
|
|
||||||
|
def nsfwProducerStore: ReadableStore[Long, NSFWProducer]
|
||||||
|
|
||||||
|
def popGeoLists: ReadableStore[String, NonPersonalizedRecommendedLists]
|
||||||
|
|
||||||
|
def listAPIStore: ReadableStore[Long, ApiList]
|
||||||
|
|
||||||
|
def openedPushByHourAggregatedStore: ReadableStore[Long, Map[Int, Int]]
|
||||||
|
|
||||||
|
def userHealthSignalStore: ReadableStore[Long, UserHealthSignalResponse]
|
||||||
|
|
||||||
|
def reactivatedUserInfoStore: ReadableStore[Long, String]
|
||||||
|
|
||||||
|
def weightedOpenOrNtabClickModelScorer: PushMLModelScorer
|
||||||
|
|
||||||
|
def optoutModelScorer: PushMLModelScorer
|
||||||
|
|
||||||
|
def filteringModelScorer: PushMLModelScorer
|
||||||
|
|
||||||
|
def recentFollowsStore: ReadableStore[Long, Seq[Long]]
|
||||||
|
|
||||||
|
def geoDuckV2Store: ReadableStore[UserId, LocationResponse]
|
||||||
|
|
||||||
|
def realGraphScoresTop500InStore: ReadableStore[Long, Map[Long, Double]]
|
||||||
|
|
||||||
|
def tweetEntityGraphStore: ReadableStore[
|
||||||
|
RecommendTweetEntityRequest,
|
||||||
|
RecommendTweetEntityResponse
|
||||||
|
]
|
||||||
|
|
||||||
|
def userUserGraphStore: ReadableStore[RecommendUserRequest, RecommendUserResponse]
|
||||||
|
|
||||||
|
def userFeaturesStore: ReadableStore[Long, UserFeatures]
|
||||||
|
|
||||||
|
def userTargetingPropertyStore: ReadableStore[Long, UserTargetingProperty]
|
||||||
|
|
||||||
|
def timelinesUserSessionStore: ReadableStore[Long, UserSession]
|
||||||
|
|
||||||
|
def optOutUserInterestsStore: ReadableStore[UserId, Seq[InterestId]]
|
||||||
|
|
||||||
|
def ntabCaretFeedbackStore: ReadableStore[GenericNotificationsFeedbackRequest, Seq[
|
||||||
|
CaretFeedbackDetails
|
||||||
|
]]
|
||||||
|
|
||||||
|
def genericFeedbackStore: ReadableStore[FeedbackRequest, Seq[
|
||||||
|
FeedbackPromptValue
|
||||||
|
]]
|
||||||
|
|
||||||
|
def genericNotificationFeedbackStore: GenericFeedbackStore
|
||||||
|
|
||||||
|
def semanticCoreMegadataStore: ReadableStore[
|
||||||
|
SemanticEntityForQuery,
|
||||||
|
EntityMegadata
|
||||||
|
]
|
||||||
|
|
||||||
|
def tweetHealthScoreStore: ReadableStore[TweetScoringRequest, TweetScoringResponse]
|
||||||
|
|
||||||
|
def earlybirdFeatureStore: ReadableStore[Long, ThriftSearchResultFeatures]
|
||||||
|
|
||||||
|
def earlybirdFeatureBuilder: FeatureBuilder[Long]
|
||||||
|
|
||||||
|
// Feature builders
|
||||||
|
|
||||||
|
def tweetAuthorLocationFeatureBuilder: FeatureBuilder[Location]
|
||||||
|
|
||||||
|
def tweetAuthorLocationFeatureBuilderById: FeatureBuilder[Long]
|
||||||
|
|
||||||
|
def socialContextActionsFeatureBuilder: FeatureBuilder[SocialContextActions]
|
||||||
|
|
||||||
|
def tweetContentFeatureBuilder: FeatureBuilder[Long]
|
||||||
|
|
||||||
|
def tweetAuthorRecentRealGraphFeatureBuilder: FeatureBuilder[RealGraphEdge]
|
||||||
|
|
||||||
|
def socialContextRecentRealGraphFeatureBuilder: FeatureBuilder[Set[RealGraphEdge]]
|
||||||
|
|
||||||
|
def tweetSocialProofFeatureBuilder: FeatureBuilder[TweetSocialProofKey]
|
||||||
|
|
||||||
|
def targetUserFullRealGraphFeatureBuilder: FeatureBuilder[TargetFullRealGraphFeatureKey]
|
||||||
|
|
||||||
|
def postProcessingFeatureBuilder: PostProcessingFeatureBuilder
|
||||||
|
|
||||||
|
def mrOfflineUserCandidateSparseAggregatesFeatureBuilder: FeatureBuilder[
|
||||||
|
OfflineSparseAggregateKey
|
||||||
|
]
|
||||||
|
|
||||||
|
def mrOfflineUserAggregatesFeatureBuilder: FeatureBuilder[Long]
|
||||||
|
|
||||||
|
def mrOfflineUserCandidateAggregatesFeatureBuilder: FeatureBuilder[OfflineAggregateKey]
|
||||||
|
|
||||||
|
def tweetAnnotationsFeatureBuilder: FeatureBuilder[Long]
|
||||||
|
|
||||||
|
def targetUserMediaRepresentationFeatureBuilder: FeatureBuilder[Long]
|
||||||
|
|
||||||
|
def targetLevelFeatureBuilder: FeatureBuilder[MrRequestContextForFeatureStore]
|
||||||
|
|
||||||
|
def candidateLevelFeatureBuilder: FeatureBuilder[EntityRequestContextForFeatureStore]
|
||||||
|
|
||||||
|
def targetFeatureHydrator: RelevanceTargetFeatureHydrator
|
||||||
|
|
||||||
|
def useHydratedLabeledSendsForFeaturesDeciderKey: String =
|
||||||
|
DeciderKey.useHydratedLabeledSendsForFeaturesDeciderKey.toString
|
||||||
|
|
||||||
|
def verifyHydratedLabeledSendsForFeaturesDeciderKey: String =
|
||||||
|
DeciderKey.verifyHydratedLabeledSendsForFeaturesDeciderKey.toString
|
||||||
|
|
||||||
|
def lexServiceStore: ReadableStore[EventRequest, LiveEvent]
|
||||||
|
|
||||||
|
def userMediaRepresentationStore: ReadableStore[Long, UserMediaRepresentation]
|
||||||
|
|
||||||
|
def producerMediaRepresentationStore: ReadableStore[Long, UserMediaRepresentation]
|
||||||
|
|
||||||
|
def mrUserStatePredictionStore: ReadableStore[Long, MRUserHmmState]
|
||||||
|
|
||||||
|
def pushcapDynamicPredictionStore: ReadableStore[Long, PushcapUserHistory]
|
||||||
|
|
||||||
|
def earlybirdCandidateSource: EarlybirdCandidateSource
|
||||||
|
|
||||||
|
def earlybirdSearchStore: ReadableStore[EarlybirdRequest, Seq[ThriftSearchResult]]
|
||||||
|
|
||||||
|
def earlybirdSearchDest: String
|
||||||
|
|
||||||
|
def pushserviceThriftClientId: ClientId
|
||||||
|
|
||||||
|
def simClusterToEntityStore: ReadableStore[Int, SimClustersInferredEntities]
|
||||||
|
|
||||||
|
def fanoutMetadataStore: ReadableStore[(Long, Long), FanoutEvent]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PostRanking Feature Store Client
|
||||||
|
*/
|
||||||
|
def postRankingFeatureStoreClient: DynamicFeatureStoreClient[MrRequestContextForFeatureStore]
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ReadableStore to fetch [[UserInterests]] from INTS service
|
||||||
|
*/
|
||||||
|
def interestsWithLookupContextStore: ReadableStore[InterestsLookupRequestWithContext, Interests]
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return: [[TopicListing]] object to fetch paused topics and scope from productId
|
||||||
|
*/
|
||||||
|
def topicListing: TopicListing
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @return: [[UttEntityHydrationStore]] object
|
||||||
|
*/
|
||||||
|
def uttEntityHydrationStore: UttEntityHydrationStore
|
||||||
|
|
||||||
|
def appPermissionStore: ReadableStore[(Long, (String, String)), AppPermission]
|
||||||
|
|
||||||
|
lazy val userTweetEntityGraphCandidates: UserTweetEntityGraphCandidates =
|
||||||
|
UserTweetEntityGraphCandidates(
|
||||||
|
cachedTweetyPieStoreV2,
|
||||||
|
tweetEntityGraphStore,
|
||||||
|
PushParams.UTEGTweetCandidateSourceParam,
|
||||||
|
PushFeatureSwitchParams.NumberOfMaxUTEGCandidatesQueriedParam,
|
||||||
|
PushParams.AllowOneSocialProofForTweetInUTEGParam,
|
||||||
|
PushParams.OutNetworkTweetsOnlyForUTEGParam,
|
||||||
|
PushFeatureSwitchParams.MaxTweetAgeParam
|
||||||
|
)(statsReceiver)
|
||||||
|
|
||||||
|
def pushSendEventBusPublisher: EventBusPublisher[NotificationScribe]
|
||||||
|
|
||||||
|
// miscs.
|
||||||
|
|
||||||
|
def isProd: Boolean
|
||||||
|
|
||||||
|
implicit def statsReceiver: StatsReceiver
|
||||||
|
|
||||||
|
def decider: Decider
|
||||||
|
|
||||||
|
def abDecider: LoggingABDecider
|
||||||
|
|
||||||
|
def casLock: CasLock
|
||||||
|
|
||||||
|
def pushIbisV2Store: PushIbis2Store
|
||||||
|
|
||||||
|
// scribe
|
||||||
|
def notificationScribe(data: NotificationScribe): Unit
|
||||||
|
|
||||||
|
def requestScribe(data: PushRequestScribe): Unit
|
||||||
|
|
||||||
|
def init(): Future[Unit] = Future.Done
|
||||||
|
|
||||||
|
def configParamsBuilder: ConfigParamsBuilder
|
||||||
|
|
||||||
|
def candidateFeatureHydrator: CandidateFeatureHydrator
|
||||||
|
|
||||||
|
def featureHydrator: MRFeatureHydrator
|
||||||
|
|
||||||
|
def candidateHydrator: PushCandidateHydrator
|
||||||
|
|
||||||
|
def sendHandlerCandidateHydrator: SendHandlerPushCandidateHydrator
|
||||||
|
|
||||||
|
lazy val overridesConfig: configapi.Config = {
|
||||||
|
val pushFeatureSwitchConfigs: configapi.Config = PushFeatureSwitches(
|
||||||
|
deciderGateBuilder = new DeciderGateBuilder(decider),
|
||||||
|
statsReceiver = statsReceiver
|
||||||
|
).config
|
||||||
|
|
||||||
|
new CompositeConfig(Seq(pushFeatureSwitchConfigs))
|
||||||
|
}
|
||||||
|
|
||||||
|
def realTimeClientEventStore: RealTimeClientEventStore
|
||||||
|
|
||||||
|
def inlineActionHistoryStore: ReadableStore[Long, Seq[(Long, String)]]
|
||||||
|
|
||||||
|
def softUserGeoLocationStore: ReadableStore[Long, GeoLocation]
|
||||||
|
|
||||||
|
def tweetTranslationStore: ReadableStore[TweetTranslationStore.Key, TweetTranslationStore.Value]
|
||||||
|
|
||||||
|
def tripTweetCandidateStore: ReadableStore[TripDomain, TripTweets]
|
||||||
|
|
||||||
|
def softUserFollowingStore: ReadableStore[User, Seq[Long]]
|
||||||
|
|
||||||
|
def superFollowEligibilityUserStore: ReadableStore[Long, Boolean]
|
||||||
|
|
||||||
|
def superFollowCreatorTweetCountStore: ReadableStore[StratoUserId, Int]
|
||||||
|
|
||||||
|
def hasSuperFollowingRelationshipStore: ReadableStore[
|
||||||
|
HasSuperFollowingRelationshipRequest,
|
||||||
|
Boolean
|
||||||
|
]
|
||||||
|
|
||||||
|
def superFollowApplicationStatusStore: ReadableStore[(Long, SellerTrack), SellerApplicationState]
|
||||||
|
|
||||||
|
def recentHistoryCacheClient: RecentHistoryCacheClient
|
||||||
|
|
||||||
|
def openAppUserStore: ReadableStore[Long, Boolean]
|
||||||
|
|
||||||
|
def loggedOutHistoryStore: PushServiceHistoryStore
|
||||||
|
|
||||||
|
def idsStore: ReadableStore[RecommendedListsRequest, RecommendedListsResponse]
|
||||||
|
|
||||||
|
def htlScoreStore(userId: Long): ReadableStore[Long, ScoredTweet]
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,16 @@
|
|||||||
|
package com.twitter.frigate.pushservice.config
|
||||||
|
|
||||||
|
import com.twitter.frigate.common.util.Experiments
|
||||||
|
|
||||||
|
object ExperimentsWithStats {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an experiment here to collect detailed pushservice stats.
|
||||||
|
*
|
||||||
|
* ! Important !
|
||||||
|
* Keep this set small and remove experiments when you don't need the stats anymore.
|
||||||
|
*/
|
||||||
|
final val PushExperiments: Set[String] = Set(
|
||||||
|
Experiments.MRAndroidInlineActionHoldback.exptName,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,230 @@
|
|||||||
|
package com.twitter.frigate.pushservice.config
|
||||||
|
|
||||||
|
import com.twitter.abdecider.LoggingABDecider
|
||||||
|
import com.twitter.bijection.scrooge.BinaryScalaCodec
|
||||||
|
import com.twitter.bijection.Base64String
|
||||||
|
import com.twitter.bijection.Injection
|
||||||
|
import com.twitter.conversions.DurationOps._
|
||||||
|
import com.twitter.decider.Decider
|
||||||
|
import com.twitter.featureswitches.v2.FeatureSwitches
|
||||||
|
import com.twitter.finagle.mtls.authentication.ServiceIdentifier
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.finagle.thrift.ClientId
|
||||||
|
import com.twitter.finagle.thrift.RichClientParam
|
||||||
|
import com.twitter.finagle.util.DefaultTimer
|
||||||
|
import com.twitter.frigate.common.config.RateLimiterGenerator
|
||||||
|
import com.twitter.frigate.common.filter.DynamicRequestMeterFilter
|
||||||
|
import com.twitter.frigate.common.history.ManhattanHistoryStore
|
||||||
|
import com.twitter.frigate.common.history.InvalidatingAfterWritesPushServiceHistoryStore
|
||||||
|
import com.twitter.frigate.common.history.ManhattanKVHistoryStore
|
||||||
|
import com.twitter.frigate.common.history.PushServiceHistoryStore
|
||||||
|
import com.twitter.frigate.common.history.SimplePushServiceHistoryStore
|
||||||
|
import com.twitter.frigate.common.util._
|
||||||
|
import com.twitter.frigate.data_pipeline.features_common.FeatureStoreUtil
|
||||||
|
import com.twitter.frigate.data_pipeline.features_common.TargetLevelFeaturesConfig
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.DeciderKey
|
||||||
|
import com.twitter.frigate.pushservice.params.PushQPSLimitConstants
|
||||||
|
import com.twitter.frigate.pushservice.params.PushServiceTunableKeys
|
||||||
|
import com.twitter.frigate.pushservice.params.ShardParams
|
||||||
|
import com.twitter.frigate.pushservice.store.PushIbis2Store
|
||||||
|
import com.twitter.frigate.pushservice.thriftscala.PushRequestScribe
|
||||||
|
import com.twitter.frigate.scribe.thriftscala.NotificationScribe
|
||||||
|
import com.twitter.ibis2.service.thriftscala.Ibis2Service
|
||||||
|
import com.twitter.logging.Logger
|
||||||
|
import com.twitter.notificationservice.api.thriftscala.DeleteCurrentTimelineForUserRequest
|
||||||
|
import com.twitter.notificationservice.api.thriftscala.NotificationApi
|
||||||
|
import com.twitter.notificationservice.api.thriftscala.NotificationApi$FinagleClient
|
||||||
|
import com.twitter.notificationservice.thriftscala.CreateGenericNotificationRequest
|
||||||
|
import com.twitter.notificationservice.thriftscala.CreateGenericNotificationResponse
|
||||||
|
import com.twitter.notificationservice.thriftscala.DeleteGenericNotificationRequest
|
||||||
|
import com.twitter.notificationservice.thriftscala.NotificationService
|
||||||
|
import com.twitter.notificationservice.thriftscala.NotificationService$FinagleClient
|
||||||
|
import com.twitter.servo.decider.DeciderGateBuilder
|
||||||
|
import com.twitter.util.tunable.TunableMap
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import com.twitter.util.Timer
|
||||||
|
|
||||||
|
case class ProdConfig(
|
||||||
|
override val isServiceLocal: Boolean,
|
||||||
|
override val localConfigRepoPath: String,
|
||||||
|
override val inMemCacheOff: Boolean,
|
||||||
|
override val decider: Decider,
|
||||||
|
override val abDecider: LoggingABDecider,
|
||||||
|
override val featureSwitches: FeatureSwitches,
|
||||||
|
override val shardParams: ShardParams,
|
||||||
|
override val serviceIdentifier: ServiceIdentifier,
|
||||||
|
override val tunableMap: TunableMap,
|
||||||
|
)(
|
||||||
|
implicit val statsReceiver: StatsReceiver)
|
||||||
|
extends {
|
||||||
|
// Due to trait initialization logic in Scala, any abstract members declared in Config or
|
||||||
|
// DeployConfig should be declared in this block. Otherwise the abstract member might initialize to
|
||||||
|
// null if invoked before object creation finishing.
|
||||||
|
|
||||||
|
val log = Logger("ProdConfig")
|
||||||
|
|
||||||
|
// Deciders
|
||||||
|
val isPushserviceCanaryDeepbirdv2CanaryClusterEnabled = decider
|
||||||
|
.feature(DeciderKey.enablePushserviceDeepbirdv2CanaryClusterDeciderKey.toString).isAvailable
|
||||||
|
|
||||||
|
// Client ids
|
||||||
|
val notifierThriftClientId = ClientId("frigate-notifier.prod")
|
||||||
|
val loggedOutNotifierThriftClientId = ClientId("frigate-logged-out-notifier.prod")
|
||||||
|
val pushserviceThriftClientId: ClientId = ClientId("frigate-pushservice.prod")
|
||||||
|
|
||||||
|
// Dests
|
||||||
|
val frigateHistoryCacheDest = "/s/cache/frigate_history"
|
||||||
|
val memcacheCASDest = "/s/cache/magic_recs_cas:twemcaches"
|
||||||
|
val historyStoreMemcacheDest =
|
||||||
|
"/srv#/prod/local/cache/magic_recs_history:twemcaches"
|
||||||
|
|
||||||
|
val deepbirdv2PredictionServiceDest =
|
||||||
|
if (serviceIdentifier.service.equals("frigate-pushservice-canary") &&
|
||||||
|
isPushserviceCanaryDeepbirdv2CanaryClusterEnabled)
|
||||||
|
"/s/frigate/deepbirdv2-magicrecs-canary"
|
||||||
|
else "/s/frigate/deepbirdv2-magicrecs"
|
||||||
|
|
||||||
|
override val fanoutMetadataColumn = "frigate/magicfanout/prod/mh/fanoutMetadata"
|
||||||
|
|
||||||
|
override val timer: Timer = DefaultTimer
|
||||||
|
override val featureStoreUtil = FeatureStoreUtil.withParams(Some(serviceIdentifier))
|
||||||
|
override val targetLevelFeaturesConfig = TargetLevelFeaturesConfig()
|
||||||
|
val pushServiceMHCacheDest = "/s/cache/pushservice_mh"
|
||||||
|
|
||||||
|
val pushServiceCoreSvcsCacheDest = "/srv#/prod/local/cache/pushservice_core_svcs"
|
||||||
|
|
||||||
|
val userTweetEntityGraphDest = "/s/cassowary/user_tweet_entity_graph"
|
||||||
|
val userUserGraphDest = "/s/cassowary/user_user_graph"
|
||||||
|
val lexServiceDest = "/s/live-video/timeline-thrift"
|
||||||
|
val entityGraphCacheDest = "/s/cache/pushservice_entity_graph"
|
||||||
|
|
||||||
|
override val pushIbisV2Store = {
|
||||||
|
val service = Finagle.readOnlyThriftService(
|
||||||
|
"ibis-v2-service",
|
||||||
|
"/s/ibis2/ibis2",
|
||||||
|
statsReceiver,
|
||||||
|
notifierThriftClientId,
|
||||||
|
requestTimeout = 3.seconds,
|
||||||
|
tries = 3,
|
||||||
|
mTLSServiceIdentifier = Some(serviceIdentifier)
|
||||||
|
)
|
||||||
|
|
||||||
|
// according to ibis team, it is safe to retry on timeout, write & channel closed exceptions.
|
||||||
|
val pushIbisClient = new Ibis2Service.FinagledClient(
|
||||||
|
new DynamicRequestMeterFilter(
|
||||||
|
tunableMap(PushServiceTunableKeys.IbisQpsLimitTunableKey),
|
||||||
|
RateLimiterGenerator.asTuple(_, shardParams.numShards, 20),
|
||||||
|
PushQPSLimitConstants.IbisOrNTabQPSForRFPH
|
||||||
|
)(timer).andThen(service),
|
||||||
|
RichClientParam(serviceName = "ibis-v2-service")
|
||||||
|
)
|
||||||
|
|
||||||
|
PushIbis2Store(pushIbisClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationServiceClient: NotificationService$FinagleClient = {
|
||||||
|
val service = Finagle.readWriteThriftService(
|
||||||
|
"notificationservice",
|
||||||
|
"/s/notificationservice/notificationservice",
|
||||||
|
statsReceiver,
|
||||||
|
pushserviceThriftClientId,
|
||||||
|
requestTimeout = 10.seconds,
|
||||||
|
mTLSServiceIdentifier = Some(serviceIdentifier)
|
||||||
|
)
|
||||||
|
|
||||||
|
new NotificationService.FinagledClient(
|
||||||
|
new DynamicRequestMeterFilter(
|
||||||
|
tunableMap(PushServiceTunableKeys.NtabQpsLimitTunableKey),
|
||||||
|
RateLimiterGenerator.asTuple(_, shardParams.numShards, 20),
|
||||||
|
PushQPSLimitConstants.IbisOrNTabQPSForRFPH)(timer).andThen(service),
|
||||||
|
RichClientParam(serviceName = "notificationservice")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationServiceApiClient: NotificationApi$FinagleClient = {
|
||||||
|
val service = Finagle.readWriteThriftService(
|
||||||
|
"notificationservice-api",
|
||||||
|
"/s/notificationservice/notificationservice-api:thrift",
|
||||||
|
statsReceiver,
|
||||||
|
pushserviceThriftClientId,
|
||||||
|
requestTimeout = 10.seconds,
|
||||||
|
mTLSServiceIdentifier = Some(serviceIdentifier)
|
||||||
|
)
|
||||||
|
|
||||||
|
new NotificationApi.FinagledClient(
|
||||||
|
new DynamicRequestMeterFilter(
|
||||||
|
tunableMap(PushServiceTunableKeys.NtabQpsLimitTunableKey),
|
||||||
|
RateLimiterGenerator.asTuple(_, shardParams.numShards, 20),
|
||||||
|
PushQPSLimitConstants.IbisOrNTabQPSForRFPH)(timer).andThen(service),
|
||||||
|
RichClientParam(serviceName = "notificationservice-api")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val mrRequestScriberNode = "mr_request_scribe"
|
||||||
|
val loggedOutMrRequestScriberNode = "lo_mr_request_scribe"
|
||||||
|
|
||||||
|
override val pushSendEventStreamName = "frigate_pushservice_send_event_prod"
|
||||||
|
} with DeployConfig {
|
||||||
|
// Scribe
|
||||||
|
private val notificationScribeLog = Logger("notification_scribe")
|
||||||
|
private val notificationScribeInjection: Injection[NotificationScribe, String] = BinaryScalaCodec(
|
||||||
|
NotificationScribe
|
||||||
|
) andThen Injection.connect[Array[Byte], Base64String, String]
|
||||||
|
|
||||||
|
override def notificationScribe(data: NotificationScribe): Unit = {
|
||||||
|
val logEntry: String = notificationScribeInjection(data)
|
||||||
|
notificationScribeLog.info(logEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// History Store - Invalidates cached history after writes
|
||||||
|
override val historyStore = new InvalidatingAfterWritesPushServiceHistoryStore(
|
||||||
|
ManhattanHistoryStore(notificationHistoryStore, statsReceiver),
|
||||||
|
recentHistoryCacheClient,
|
||||||
|
new DeciderGateBuilder(decider)
|
||||||
|
.idGate(DeciderKey.enableInvalidatingCachedHistoryStoreAfterWrites)
|
||||||
|
)
|
||||||
|
|
||||||
|
override val emailHistoryStore: PushServiceHistoryStore = {
|
||||||
|
statsReceiver.scope("frigate_email_history").counter("request").incr()
|
||||||
|
new SimplePushServiceHistoryStore(emailNotificationHistoryStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
override val loggedOutHistoryStore =
|
||||||
|
new InvalidatingAfterWritesPushServiceHistoryStore(
|
||||||
|
ManhattanKVHistoryStore(
|
||||||
|
manhattanKVLoggedOutHistoryStoreEndpoint,
|
||||||
|
"frigate_notification_logged_out_history"),
|
||||||
|
recentHistoryCacheClient,
|
||||||
|
new DeciderGateBuilder(decider)
|
||||||
|
.idGate(DeciderKey.enableInvalidatingCachedLoggedOutHistoryStoreAfterWrites)
|
||||||
|
)
|
||||||
|
|
||||||
|
private val requestScribeLog = Logger("request_scribe")
|
||||||
|
private val requestScribeInjection: Injection[PushRequestScribe, String] = BinaryScalaCodec(
|
||||||
|
PushRequestScribe
|
||||||
|
) andThen Injection.connect[Array[Byte], Base64String, String]
|
||||||
|
|
||||||
|
override def requestScribe(data: PushRequestScribe): Unit = {
|
||||||
|
val logEntry: String = requestScribeInjection(data)
|
||||||
|
requestScribeLog.info(logEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// generic notification server
|
||||||
|
override def notificationServiceSend(
|
||||||
|
target: Target,
|
||||||
|
request: CreateGenericNotificationRequest
|
||||||
|
): Future[CreateGenericNotificationResponse] =
|
||||||
|
notificationServiceClient.createGenericNotification(request)
|
||||||
|
|
||||||
|
// generic notification server
|
||||||
|
override def notificationServiceDelete(
|
||||||
|
request: DeleteGenericNotificationRequest
|
||||||
|
): Future[Unit] = notificationServiceClient.deleteGenericNotification(request)
|
||||||
|
|
||||||
|
// NTab-api
|
||||||
|
override def notificationServiceDeleteTimeline(
|
||||||
|
request: DeleteCurrentTimelineForUserRequest
|
||||||
|
): Future[Unit] = notificationServiceApiClient.deleteCurrentTimelineForUser(request)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,193 @@
|
|||||||
|
package com.twitter.frigate.pushservice.config
|
||||||
|
|
||||||
|
import com.twitter.abdecider.LoggingABDecider
|
||||||
|
import com.twitter.conversions.DurationOps._
|
||||||
|
import com.twitter.decider.Decider
|
||||||
|
import com.twitter.featureswitches.v2.FeatureSwitches
|
||||||
|
import com.twitter.finagle.mtls.authentication.ServiceIdentifier
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.finagle.thrift.ClientId
|
||||||
|
import com.twitter.finagle.thrift.RichClientParam
|
||||||
|
import com.twitter.finagle.util.DefaultTimer
|
||||||
|
import com.twitter.frigate.common.config.RateLimiterGenerator
|
||||||
|
import com.twitter.frigate.common.filter.DynamicRequestMeterFilter
|
||||||
|
import com.twitter.frigate.common.history.InvalidatingAfterWritesPushServiceHistoryStore
|
||||||
|
import com.twitter.frigate.common.history.ManhattanHistoryStore
|
||||||
|
import com.twitter.frigate.common.history.ManhattanKVHistoryStore
|
||||||
|
import com.twitter.frigate.common.history.ReadOnlyHistoryStore
|
||||||
|
import com.twitter.frigate.common.history.PushServiceHistoryStore
|
||||||
|
import com.twitter.frigate.common.history.SimplePushServiceHistoryStore
|
||||||
|
import com.twitter.frigate.common.util.Finagle
|
||||||
|
import com.twitter.frigate.data_pipeline.features_common.FeatureStoreUtil
|
||||||
|
import com.twitter.frigate.data_pipeline.features_common.TargetLevelFeaturesConfig
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.DeciderKey
|
||||||
|
import com.twitter.frigate.pushservice.params.PushQPSLimitConstants
|
||||||
|
import com.twitter.frigate.pushservice.params.PushServiceTunableKeys
|
||||||
|
import com.twitter.frigate.pushservice.params.ShardParams
|
||||||
|
import com.twitter.frigate.pushservice.store._
|
||||||
|
import com.twitter.frigate.pushservice.thriftscala.PushRequestScribe
|
||||||
|
import com.twitter.frigate.scribe.thriftscala.NotificationScribe
|
||||||
|
import com.twitter.ibis2.service.thriftscala.Ibis2Service
|
||||||
|
import com.twitter.logging.Logger
|
||||||
|
import com.twitter.notificationservice.api.thriftscala.DeleteCurrentTimelineForUserRequest
|
||||||
|
import com.twitter.notificationservice.thriftscala.CreateGenericNotificationRequest
|
||||||
|
import com.twitter.notificationservice.thriftscala.CreateGenericNotificationResponse
|
||||||
|
import com.twitter.notificationservice.thriftscala.CreateGenericNotificationResponseType
|
||||||
|
import com.twitter.notificationservice.thriftscala.DeleteGenericNotificationRequest
|
||||||
|
import com.twitter.notificationservice.thriftscala.NotificationService
|
||||||
|
import com.twitter.notificationservice.thriftscala.NotificationService$FinagleClient
|
||||||
|
import com.twitter.servo.decider.DeciderGateBuilder
|
||||||
|
import com.twitter.util.tunable.TunableMap
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import com.twitter.util.Timer
|
||||||
|
|
||||||
|
case class StagingConfig(
|
||||||
|
override val isServiceLocal: Boolean,
|
||||||
|
override val localConfigRepoPath: String,
|
||||||
|
override val inMemCacheOff: Boolean,
|
||||||
|
override val decider: Decider,
|
||||||
|
override val abDecider: LoggingABDecider,
|
||||||
|
override val featureSwitches: FeatureSwitches,
|
||||||
|
override val shardParams: ShardParams,
|
||||||
|
override val serviceIdentifier: ServiceIdentifier,
|
||||||
|
override val tunableMap: TunableMap,
|
||||||
|
)(
|
||||||
|
implicit val statsReceiver: StatsReceiver)
|
||||||
|
extends {
|
||||||
|
// Due to trait initialization logic in Scala, any abstract members declared in Config or
|
||||||
|
// DeployConfig should be declared in this block. Otherwise the abstract member might initialize to
|
||||||
|
// null if invoked before object creation finishing.
|
||||||
|
|
||||||
|
val log = Logger("StagingConfig")
|
||||||
|
|
||||||
|
// Client ids
|
||||||
|
val notifierThriftClientId = ClientId("frigate-notifier.dev")
|
||||||
|
val loggedOutNotifierThriftClientId = ClientId("frigate-logged-out-notifier.dev")
|
||||||
|
val pushserviceThriftClientId: ClientId = ClientId("frigate-pushservice.staging")
|
||||||
|
|
||||||
|
override val fanoutMetadataColumn = "frigate/magicfanout/staging/mh/fanoutMetadata"
|
||||||
|
|
||||||
|
// dest
|
||||||
|
val frigateHistoryCacheDest = "/srv#/test/local/cache/twemcache_frigate_history"
|
||||||
|
val memcacheCASDest = "/srv#/test/local/cache/twemcache_magic_recs_cas_dev:twemcaches"
|
||||||
|
val pushServiceMHCacheDest = "/srv#/test/local/cache/twemcache_pushservice_test"
|
||||||
|
val entityGraphCacheDest = "/srv#/test/local/cache/twemcache_pushservice_test"
|
||||||
|
val pushServiceCoreSvcsCacheDest = "/srv#/test/local/cache/twemcache_pushservice_core_svcs_test"
|
||||||
|
val historyStoreMemcacheDest = "/srv#/test/local/cache/twemcache_eventstream_test:twemcaches"
|
||||||
|
val userTweetEntityGraphDest = "/cluster/local/cassowary/staging/user_tweet_entity_graph"
|
||||||
|
val userUserGraphDest = "/cluster/local/cassowary/staging/user_user_graph"
|
||||||
|
val lexServiceDest = "/srv#/staging/local/live-video/timeline-thrift"
|
||||||
|
val deepbirdv2PredictionServiceDest = "/cluster/local/frigate/staging/deepbirdv2-magicrecs"
|
||||||
|
|
||||||
|
override val featureStoreUtil = FeatureStoreUtil.withParams(Some(serviceIdentifier))
|
||||||
|
override val targetLevelFeaturesConfig = TargetLevelFeaturesConfig()
|
||||||
|
val mrRequestScriberNode = "validation_mr_request_scribe"
|
||||||
|
val loggedOutMrRequestScriberNode = "lo_mr_request_scribe"
|
||||||
|
|
||||||
|
override val timer: Timer = DefaultTimer
|
||||||
|
|
||||||
|
override val pushSendEventStreamName = "frigate_pushservice_send_event_staging"
|
||||||
|
|
||||||
|
override val pushIbisV2Store = {
|
||||||
|
val service = Finagle.readWriteThriftService(
|
||||||
|
"ibis-v2-service",
|
||||||
|
"/s/ibis2/ibis2",
|
||||||
|
statsReceiver,
|
||||||
|
notifierThriftClientId,
|
||||||
|
requestTimeout = 6.seconds,
|
||||||
|
mTLSServiceIdentifier = Some(serviceIdentifier)
|
||||||
|
)
|
||||||
|
|
||||||
|
val pushIbisClient = new Ibis2Service.FinagledClient(
|
||||||
|
new DynamicRequestMeterFilter(
|
||||||
|
tunableMap(PushServiceTunableKeys.IbisQpsLimitTunableKey),
|
||||||
|
RateLimiterGenerator.asTuple(_, shardParams.numShards, 20),
|
||||||
|
PushQPSLimitConstants.IbisOrNTabQPSForRFPH
|
||||||
|
)(timer).andThen(service),
|
||||||
|
RichClientParam(serviceName = "ibis-v2-service")
|
||||||
|
)
|
||||||
|
|
||||||
|
StagingIbis2Store(PushIbis2Store(pushIbisClient))
|
||||||
|
}
|
||||||
|
|
||||||
|
val notificationServiceClient: NotificationService$FinagleClient = {
|
||||||
|
val service = Finagle.readWriteThriftService(
|
||||||
|
"notificationservice",
|
||||||
|
"/s/notificationservice/notificationservice",
|
||||||
|
statsReceiver,
|
||||||
|
pushserviceThriftClientId,
|
||||||
|
requestTimeout = 10.seconds,
|
||||||
|
mTLSServiceIdentifier = Some(serviceIdentifier)
|
||||||
|
)
|
||||||
|
|
||||||
|
new NotificationService.FinagledClient(
|
||||||
|
new DynamicRequestMeterFilter(
|
||||||
|
tunableMap(PushServiceTunableKeys.NtabQpsLimitTunableKey),
|
||||||
|
RateLimiterGenerator.asTuple(_, shardParams.numShards, 20),
|
||||||
|
PushQPSLimitConstants.IbisOrNTabQPSForRFPH)(timer).andThen(service),
|
||||||
|
RichClientParam(serviceName = "notificationservice")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} with DeployConfig {
|
||||||
|
|
||||||
|
// Scribe
|
||||||
|
private val notificationScribeLog = Logger("StagingNotificationScribe")
|
||||||
|
|
||||||
|
override def notificationScribe(data: NotificationScribe): Unit = {
|
||||||
|
notificationScribeLog.info(data.toString)
|
||||||
|
}
|
||||||
|
private val requestScribeLog = Logger("StagingRequestScribe")
|
||||||
|
|
||||||
|
override def requestScribe(data: PushRequestScribe): Unit = {
|
||||||
|
requestScribeLog.info(data.toString)
|
||||||
|
}
|
||||||
|
|
||||||
|
// history store
|
||||||
|
override val historyStore = new InvalidatingAfterWritesPushServiceHistoryStore(
|
||||||
|
ReadOnlyHistoryStore(
|
||||||
|
ManhattanHistoryStore(notificationHistoryStore, statsReceiver)
|
||||||
|
),
|
||||||
|
recentHistoryCacheClient,
|
||||||
|
new DeciderGateBuilder(decider)
|
||||||
|
.idGate(DeciderKey.enableInvalidatingCachedHistoryStoreAfterWrites)
|
||||||
|
)
|
||||||
|
|
||||||
|
override val emailHistoryStore: PushServiceHistoryStore = new SimplePushServiceHistoryStore(
|
||||||
|
emailNotificationHistoryStore)
|
||||||
|
|
||||||
|
// history store
|
||||||
|
override val loggedOutHistoryStore =
|
||||||
|
new InvalidatingAfterWritesPushServiceHistoryStore(
|
||||||
|
ReadOnlyHistoryStore(
|
||||||
|
ManhattanKVHistoryStore(
|
||||||
|
manhattanKVLoggedOutHistoryStoreEndpoint,
|
||||||
|
"frigate_notification_logged_out_history")),
|
||||||
|
recentHistoryCacheClient,
|
||||||
|
new DeciderGateBuilder(decider)
|
||||||
|
.idGate(DeciderKey.enableInvalidatingCachedLoggedOutHistoryStoreAfterWrites)
|
||||||
|
)
|
||||||
|
|
||||||
|
override def notificationServiceSend(
|
||||||
|
target: Target,
|
||||||
|
request: CreateGenericNotificationRequest
|
||||||
|
): Future[CreateGenericNotificationResponse] =
|
||||||
|
target.isTeamMember.flatMap { isTeamMember =>
|
||||||
|
if (isTeamMember) {
|
||||||
|
notificationServiceClient.createGenericNotification(request)
|
||||||
|
} else {
|
||||||
|
log.info(s"Mock creating generic notification $request for user: ${target.targetId}")
|
||||||
|
Future.value(
|
||||||
|
CreateGenericNotificationResponse(CreateGenericNotificationResponseType.Success)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override def notificationServiceDelete(
|
||||||
|
request: DeleteGenericNotificationRequest
|
||||||
|
): Future[Unit] = Future.Unit
|
||||||
|
|
||||||
|
override def notificationServiceDeleteTimeline(
|
||||||
|
request: DeleteCurrentTimelineForUserRequest
|
||||||
|
): Future[Unit] = Future.Unit
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package com.twitter.frigate.pushservice.config.mlconfig
|
||||||
|
|
||||||
|
import com.twitter.cortex.deepbird.thriftjava.DeepbirdPredictionService
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.ml.prediction.DeepbirdPredictionEngineServiceStore
|
||||||
|
import com.twitter.nrel.heavyranker.PushDBv2PredictionServiceStore
|
||||||
|
|
||||||
|
object DeepbirdV2ModelConfig {
|
||||||
|
def buildPredictionServiceScoreStore(
|
||||||
|
predictionServiceClient: DeepbirdPredictionService.ServiceToClient,
|
||||||
|
serviceName: String
|
||||||
|
)(
|
||||||
|
implicit statsReceiver: StatsReceiver
|
||||||
|
): PushDBv2PredictionServiceStore = {
|
||||||
|
|
||||||
|
val stats = statsReceiver.scope(serviceName)
|
||||||
|
val serviceStats = statsReceiver.scope("dbv2PredictionServiceStore")
|
||||||
|
|
||||||
|
new PushDBv2PredictionServiceStore(
|
||||||
|
DeepbirdPredictionEngineServiceStore(predictionServiceClient, batchSize = Some(32))(stats)
|
||||||
|
)(serviceStats)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,114 @@
|
|||||||
|
package com.twitter.frigate.pushservice.controller
|
||||||
|
|
||||||
|
import com.google.inject.Inject
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.finagle.thrift.ClientId
|
||||||
|
import com.twitter.finatra.thrift.Controller
|
||||||
|
import com.twitter.frigate.pushservice.exception.DisplayLocationNotSupportedException
|
||||||
|
import com.twitter.frigate.pushservice.refresh_handler.RefreshForPushHandler
|
||||||
|
import com.twitter.frigate.pushservice.send_handler.SendHandler
|
||||||
|
import com.twitter.frigate.pushservice.refresh_handler.LoggedOutRefreshForPushHandler
|
||||||
|
import com.twitter.frigate.pushservice.thriftscala.PushService.Loggedout
|
||||||
|
import com.twitter.frigate.pushservice.thriftscala.PushService.Refresh
|
||||||
|
import com.twitter.frigate.pushservice.thriftscala.PushService.Send
|
||||||
|
import com.twitter.frigate.pushservice.{thriftscala => t}
|
||||||
|
import com.twitter.frigate.thriftscala.NotificationDisplayLocation
|
||||||
|
import com.twitter.util.logging.Logging
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class PushServiceController @Inject() (
|
||||||
|
sendHandler: SendHandler,
|
||||||
|
refreshForPushHandler: RefreshForPushHandler,
|
||||||
|
loggedOutRefreshForPushHandler: LoggedOutRefreshForPushHandler,
|
||||||
|
statsReceiver: StatsReceiver)
|
||||||
|
extends Controller(t.PushService)
|
||||||
|
with Logging {
|
||||||
|
|
||||||
|
private val stats: StatsReceiver = statsReceiver.scope(s"${this.getClass.getSimpleName}")
|
||||||
|
private val failureCount = stats.counter("failures")
|
||||||
|
private val failureStatsScope = stats.scope("failures")
|
||||||
|
private val uncaughtErrorCount = failureStatsScope.counter("uncaught")
|
||||||
|
private val uncaughtErrorScope = failureStatsScope.scope("uncaught")
|
||||||
|
private val clientIdScope = stats.scope("client_id")
|
||||||
|
|
||||||
|
handle(t.PushService.Send) { request: Send.Args =>
|
||||||
|
send(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle(t.PushService.Refresh) { args: Refresh.Args =>
|
||||||
|
refresh(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
handle(t.PushService.Loggedout) { request: Loggedout.Args =>
|
||||||
|
loggedOutRefresh(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def loggedOutRefresh(
|
||||||
|
request: t.PushService.Loggedout.Args
|
||||||
|
): Future[t.PushService.Loggedout.SuccessType] = {
|
||||||
|
val fut = request.request.notificationDisplayLocation match {
|
||||||
|
case NotificationDisplayLocation.PushToMobileDevice =>
|
||||||
|
loggedOutRefreshForPushHandler.refreshAndSend(request.request)
|
||||||
|
case _ =>
|
||||||
|
Future.exception(
|
||||||
|
new DisplayLocationNotSupportedException(
|
||||||
|
"Specified notification display location is not supported"))
|
||||||
|
}
|
||||||
|
fut.onFailure { ex =>
|
||||||
|
logger.error(
|
||||||
|
s"Failure in push service for logged out refresh request: $request - ${ex.getMessage} - ${ex.getStackTrace
|
||||||
|
.mkString(", \n\t")}",
|
||||||
|
ex)
|
||||||
|
failureCount.incr()
|
||||||
|
uncaughtErrorCount.incr()
|
||||||
|
uncaughtErrorScope.counter(ex.getClass.getCanonicalName).incr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def refresh(
|
||||||
|
request: t.PushService.Refresh.Args
|
||||||
|
): Future[t.PushService.Refresh.SuccessType] = {
|
||||||
|
|
||||||
|
val fut = request.request.notificationDisplayLocation match {
|
||||||
|
case NotificationDisplayLocation.PushToMobileDevice =>
|
||||||
|
val clientId: String =
|
||||||
|
ClientId.current
|
||||||
|
.flatMap { cid => Option(cid.name) }
|
||||||
|
.getOrElse("none")
|
||||||
|
clientIdScope.counter(clientId).incr()
|
||||||
|
refreshForPushHandler.refreshAndSend(request.request)
|
||||||
|
case _ =>
|
||||||
|
Future.exception(
|
||||||
|
new DisplayLocationNotSupportedException(
|
||||||
|
"Specified notification display location is not supported"))
|
||||||
|
}
|
||||||
|
fut.onFailure { ex =>
|
||||||
|
logger.error(
|
||||||
|
s"Failure in push service for refresh request: $request - ${ex.getMessage} - ${ex.getStackTrace
|
||||||
|
.mkString(", \n\t")}",
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
|
||||||
|
failureCount.incr()
|
||||||
|
uncaughtErrorCount.incr()
|
||||||
|
uncaughtErrorScope.counter(ex.getClass.getCanonicalName).incr()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private def send(
|
||||||
|
request: t.PushService.Send.Args
|
||||||
|
): Future[t.PushService.Send.SuccessType] = {
|
||||||
|
sendHandler(request.request).onFailure { ex =>
|
||||||
|
logger.error(
|
||||||
|
s"Failure in push service for send request: $request - ${ex.getMessage} - ${ex.getStackTrace
|
||||||
|
.mkString(", \n\t")}",
|
||||||
|
ex
|
||||||
|
)
|
||||||
|
|
||||||
|
failureCount.incr()
|
||||||
|
uncaughtErrorCount.incr()
|
||||||
|
uncaughtErrorScope.counter(ex.getClass.getCanonicalName).incr()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.twitter.frigate.pushservice.exception
|
||||||
|
|
||||||
|
import scala.util.control.NoStackTrace
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw exception if DisplayLocation is not supported
|
||||||
|
*
|
||||||
|
* @param message Exception message
|
||||||
|
*/
|
||||||
|
class DisplayLocationNotSupportedException(private val message: String)
|
||||||
|
extends Exception(message)
|
||||||
|
with NoStackTrace
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.twitter.frigate.pushservice.exception
|
||||||
|
|
||||||
|
import scala.util.control.NoStackTrace
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw exception if the sport domain is not supported by MagicFanoutSports
|
||||||
|
*
|
||||||
|
* @param message Exception message
|
||||||
|
*/
|
||||||
|
class InvalidSportDomainException(private val message: String)
|
||||||
|
extends Exception(message)
|
||||||
|
with NoStackTrace
|
@ -0,0 +1,7 @@
|
|||||||
|
package com.twitter.frigate.pushservice.exception
|
||||||
|
|
||||||
|
import scala.util.control.NoStackTrace
|
||||||
|
|
||||||
|
class TweetNTabRequestHydratorException(private val message: String)
|
||||||
|
extends Exception(message)
|
||||||
|
with NoStackTrace
|
@ -0,0 +1,11 @@
|
|||||||
|
package com.twitter.frigate.pushservice.exception
|
||||||
|
|
||||||
|
import scala.util.control.NoStackTrace
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exception for CRT not expected in the scope
|
||||||
|
* @param message Exception message to log the UnsupportedCrt
|
||||||
|
*/
|
||||||
|
class UnsupportedCrtException(private val message: String)
|
||||||
|
extends Exception(message)
|
||||||
|
with NoStackTrace
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.twitter.frigate.pushservice.exception
|
||||||
|
|
||||||
|
import scala.util.control.NoStackTrace
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Throw exception if UttEntity is not found where it might be a required data field
|
||||||
|
*
|
||||||
|
* @param message Exception message
|
||||||
|
*/
|
||||||
|
class UttEntityNotFoundException(private val message: String)
|
||||||
|
extends Exception(message)
|
||||||
|
with NoStackTrace
|
@ -0,0 +1,220 @@
|
|||||||
|
package com.twitter.frigate.pushservice.ml
|
||||||
|
|
||||||
|
import com.twitter.abuse.detection.scoring.thriftscala.{Model => TweetHealthModel}
|
||||||
|
import com.twitter.abuse.detection.scoring.thriftscala.TweetScoringRequest
|
||||||
|
import com.twitter.abuse.detection.scoring.thriftscala.TweetScoringResponse
|
||||||
|
import com.twitter.frigate.common.base.FeatureMap
|
||||||
|
import com.twitter.frigate.common.base.TweetAuthor
|
||||||
|
import com.twitter.frigate.common.base.TweetAuthorDetails
|
||||||
|
import com.twitter.frigate.common.base.TweetCandidate
|
||||||
|
import com.twitter.frigate.common.rec_types.RecTypes
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.params.PushConstants
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.predicate.HealthPredicates.userHealthSignalValueToDouble
|
||||||
|
import com.twitter.frigate.pushservice.util.CandidateHydrationUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.CandidateUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.MediaAnnotationsUtil
|
||||||
|
import com.twitter.frigate.thriftscala.UserMediaRepresentation
|
||||||
|
import com.twitter.hss.api.thriftscala.SignalValue
|
||||||
|
import com.twitter.hss.api.thriftscala.UserHealthSignal
|
||||||
|
import com.twitter.hss.api.thriftscala.UserHealthSignal.AgathaCalibratedNsfwDouble
|
||||||
|
import com.twitter.hss.api.thriftscala.UserHealthSignal.NsfwTextUserScoreDouble
|
||||||
|
import com.twitter.hss.api.thriftscala.UserHealthSignalResponse
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import com.twitter.util.Time
|
||||||
|
|
||||||
|
object HealthFeatureGetter {
|
||||||
|
|
||||||
|
def getFeatures(
|
||||||
|
pushCandidate: PushCandidate,
|
||||||
|
producerMediaRepresentationStore: ReadableStore[Long, UserMediaRepresentation],
|
||||||
|
userHealthScoreStore: ReadableStore[Long, UserHealthSignalResponse],
|
||||||
|
tweetHealthScoreStoreOpt: Option[ReadableStore[TweetScoringRequest, TweetScoringResponse]] =
|
||||||
|
None
|
||||||
|
): Future[FeatureMap] = {
|
||||||
|
|
||||||
|
pushCandidate match {
|
||||||
|
case cand: PushCandidate with TweetCandidate with TweetAuthor with TweetAuthorDetails =>
|
||||||
|
val pMediaNsfwRequest =
|
||||||
|
TweetScoringRequest(cand.tweetId, TweetHealthModel.ExperimentalHealthModelScore4)
|
||||||
|
val pTweetTextNsfwRequest =
|
||||||
|
TweetScoringRequest(cand.tweetId, TweetHealthModel.ExperimentalHealthModelScore1)
|
||||||
|
|
||||||
|
cand.authorId match {
|
||||||
|
case Some(authorId) =>
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
userHealthScoreStore.get(authorId),
|
||||||
|
producerMediaRepresentationStore.get(authorId),
|
||||||
|
tweetHealthScoreStoreOpt.map(_.get(pMediaNsfwRequest)).getOrElse(Future.None),
|
||||||
|
tweetHealthScoreStoreOpt.map(_.get(pTweetTextNsfwRequest)).getOrElse(Future.None),
|
||||||
|
cand.tweetAuthor
|
||||||
|
).map {
|
||||||
|
case (
|
||||||
|
healthSignalsResponseOpt,
|
||||||
|
producerMuOpt,
|
||||||
|
pMediaNsfwOpt,
|
||||||
|
pTweetTextNsfwOpt,
|
||||||
|
tweetAuthorOpt) =>
|
||||||
|
val healthSignalScoreMap = healthSignalsResponseOpt
|
||||||
|
.map(_.signalValues).getOrElse(Map.empty[UserHealthSignal, SignalValue])
|
||||||
|
val agathaNSFWScore = userHealthSignalValueToDouble(
|
||||||
|
healthSignalScoreMap
|
||||||
|
.getOrElse(AgathaCalibratedNsfwDouble, SignalValue.DoubleValue(0.5)))
|
||||||
|
val userTextNSFWScore = userHealthSignalValueToDouble(
|
||||||
|
healthSignalScoreMap
|
||||||
|
.getOrElse(NsfwTextUserScoreDouble, SignalValue.DoubleValue(0.15)))
|
||||||
|
val pMediaNsfwScore = pMediaNsfwOpt.map(_.score).getOrElse(0.0)
|
||||||
|
val pTweetTextNsfwScore = pTweetTextNsfwOpt.map(_.score).getOrElse(0.0)
|
||||||
|
|
||||||
|
val mediaRepresentationMap =
|
||||||
|
producerMuOpt.map(_.mediaRepresentation).getOrElse(Map.empty[String, Double])
|
||||||
|
val sumScore: Double = mediaRepresentationMap.values.sum
|
||||||
|
val nudityRate =
|
||||||
|
if (sumScore > 0)
|
||||||
|
mediaRepresentationMap.getOrElse(
|
||||||
|
MediaAnnotationsUtil.nudityCategoryId,
|
||||||
|
0.0) / sumScore
|
||||||
|
else 0.0
|
||||||
|
val beautyRate =
|
||||||
|
if (sumScore > 0)
|
||||||
|
mediaRepresentationMap.getOrElse(
|
||||||
|
MediaAnnotationsUtil.beautyCategoryId,
|
||||||
|
0.0) / sumScore
|
||||||
|
else 0.0
|
||||||
|
val singlePersonRate =
|
||||||
|
if (sumScore > 0)
|
||||||
|
mediaRepresentationMap.getOrElse(
|
||||||
|
MediaAnnotationsUtil.singlePersonCategoryId,
|
||||||
|
0.0) / sumScore
|
||||||
|
else 0.0
|
||||||
|
val dislikeCt = cand.numericFeatures.getOrElse(
|
||||||
|
"tweet.magic_recs_tweet_real_time_aggregates_v2.pair.v2.magicrecs.realtime.is_ntab_disliked.any_feature.Duration.Top.count",
|
||||||
|
0.0)
|
||||||
|
val sentCt = cand.numericFeatures.getOrElse(
|
||||||
|
"tweet.magic_recs_tweet_real_time_aggregates_v2.pair.v2.magicrecs.realtime.is_sent.any_feature.Duration.Top.count",
|
||||||
|
0.0)
|
||||||
|
val dislikeRate = if (sentCt > 0) dislikeCt / sentCt else 0.0
|
||||||
|
|
||||||
|
val authorDislikeCt = cand.numericFeatures.getOrElse(
|
||||||
|
"tweet_author_aggregate.pair.label.ntab.isDisliked.any_feature.28.days.count",
|
||||||
|
0.0)
|
||||||
|
val authorReportCt = cand.numericFeatures.getOrElse(
|
||||||
|
"tweet_author_aggregate.pair.label.reportTweetDone.any_feature.28.days.count",
|
||||||
|
0.0)
|
||||||
|
val authorSentCt = cand.numericFeatures
|
||||||
|
.getOrElse(
|
||||||
|
"tweet_author_aggregate.pair.any_label.any_feature.28.days.count",
|
||||||
|
0.0)
|
||||||
|
val authorDislikeRate =
|
||||||
|
if (authorSentCt > 0) authorDislikeCt / authorSentCt else 0.0
|
||||||
|
val authorReportRate =
|
||||||
|
if (authorSentCt > 0) authorReportCt / authorSentCt else 0.0
|
||||||
|
|
||||||
|
val (isNsfwAccount, authorAccountAge) = tweetAuthorOpt match {
|
||||||
|
case Some(tweetAuthor) =>
|
||||||
|
(
|
||||||
|
CandidateHydrationUtil.isNsfwAccount(
|
||||||
|
tweetAuthor,
|
||||||
|
cand.target.params(PushFeatureSwitchParams.NsfwTokensParam)),
|
||||||
|
(Time.now - Time.fromMilliseconds(tweetAuthor.createdAtMsec)).inHours
|
||||||
|
)
|
||||||
|
case _ => (false, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
val tweetSemanticCoreIds = cand.sparseBinaryFeatures
|
||||||
|
.getOrElse(PushConstants.TweetSemanticCoreIdFeature, Set.empty[String])
|
||||||
|
|
||||||
|
val continuousFeatures = Map[String, Double](
|
||||||
|
"agathaNsfwScore" -> agathaNSFWScore,
|
||||||
|
"textNsfwScore" -> userTextNSFWScore,
|
||||||
|
"pMediaNsfwScore" -> pMediaNsfwScore,
|
||||||
|
"pTweetTextNsfwScore" -> pTweetTextNsfwScore,
|
||||||
|
"nudityRate" -> nudityRate,
|
||||||
|
"beautyRate" -> beautyRate,
|
||||||
|
"singlePersonRate" -> singlePersonRate,
|
||||||
|
"numSources" -> CandidateUtil.getTagsCRCount(cand),
|
||||||
|
"favCount" -> cand.numericFeatures
|
||||||
|
.getOrElse("tweet.core.tweet_counts.favorite_count", 0.0),
|
||||||
|
"activeFollowers" -> cand.numericFeatures
|
||||||
|
.getOrElse("RecTweetAuthor.User.ActiveFollowers", 0.0),
|
||||||
|
"favorsRcvd28Days" -> cand.numericFeatures
|
||||||
|
.getOrElse("RecTweetAuthor.User.FavorsRcvd28Days", 0.0),
|
||||||
|
"tweets28Days" -> cand.numericFeatures
|
||||||
|
.getOrElse("RecTweetAuthor.User.Tweets28Days", 0.0),
|
||||||
|
"dislikeCount" -> dislikeCt,
|
||||||
|
"dislikeRate" -> dislikeRate,
|
||||||
|
"sentCount" -> sentCt,
|
||||||
|
"authorDislikeCount" -> authorDislikeCt,
|
||||||
|
"authorDislikeRate" -> authorDislikeRate,
|
||||||
|
"authorReportCount" -> authorReportCt,
|
||||||
|
"authorReportRate" -> authorReportRate,
|
||||||
|
"authorSentCount" -> authorSentCt,
|
||||||
|
"authorAgeInHour" -> authorAccountAge.toDouble
|
||||||
|
)
|
||||||
|
|
||||||
|
val booleanFeatures = Map[String, Boolean](
|
||||||
|
"isSimclusterBased" -> RecTypes.simclusterBasedTweets
|
||||||
|
.contains(cand.commonRecType),
|
||||||
|
"isTopicTweet" -> RecTypes.isTopicTweetType(cand.commonRecType),
|
||||||
|
"isHashSpace" -> RecTypes.tagspaceTypes.contains(cand.commonRecType),
|
||||||
|
"isFRS" -> RecTypes.frsTypes.contains(cand.commonRecType),
|
||||||
|
"isModelingBased" -> RecTypes.mrModelingBasedTypes.contains(cand.commonRecType),
|
||||||
|
"isGeoPop" -> RecTypes.GeoPopTweetTypes.contains(cand.commonRecType),
|
||||||
|
"hasPhoto" -> cand.booleanFeatures
|
||||||
|
.getOrElse("RecTweet.TweetyPieResult.HasPhoto", false),
|
||||||
|
"hasVideo" -> cand.booleanFeatures
|
||||||
|
.getOrElse("RecTweet.TweetyPieResult.HasVideo", false),
|
||||||
|
"hasUrl" -> cand.booleanFeatures
|
||||||
|
.getOrElse("RecTweet.TweetyPieResult.HasUrl", false),
|
||||||
|
"isMrTwistly" -> CandidateUtil.isMrTwistlyCandidate(cand),
|
||||||
|
"abuseStrikeTop2Percent" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.AbuseStrike_Top2Percent_Id),
|
||||||
|
"abuseStrikeTop1Percent" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.AbuseStrike_Top1Percent_Id),
|
||||||
|
"abuseStrikeTop05Percent" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.AbuseStrike_Top05Percent_Id),
|
||||||
|
"abuseStrikeTop025Percent" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.AbuseStrike_Top025Percent_Id),
|
||||||
|
"allSpamReportsPerFavTop1Percent" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.AllSpamReportsPerFav_Top1Percent_Id),
|
||||||
|
"reportsPerFavTop1Percent" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.ReportsPerFav_Top1Percent_Id),
|
||||||
|
"reportsPerFavTop2Percent" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.ReportsPerFav_Top2Percent_Id),
|
||||||
|
"isNudity" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.MediaUnderstanding_Nudity_Id),
|
||||||
|
"beautyStyleFashion" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.MediaUnderstanding_Beauty_Id),
|
||||||
|
"singlePerson" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.MediaUnderstanding_SinglePerson_Id),
|
||||||
|
"pornList" -> tweetSemanticCoreIds.contains(PushConstants.PornList_Id),
|
||||||
|
"pornographyAndNsfwContent" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.PornographyAndNsfwContent_Id),
|
||||||
|
"sexLife" -> tweetSemanticCoreIds.contains(PushConstants.SexLife_Id),
|
||||||
|
"sexLifeOrSexualOrientation" -> tweetSemanticCoreIds.contains(
|
||||||
|
PushConstants.SexLifeOrSexualOrientation_Id),
|
||||||
|
"profanity" -> tweetSemanticCoreIds.contains(PushConstants.ProfanityFilter_Id),
|
||||||
|
"isVerified" -> cand.booleanFeatures
|
||||||
|
.getOrElse("RecTweetAuthor.User.IsVerified", false),
|
||||||
|
"hasNsfwToken" -> isNsfwAccount
|
||||||
|
)
|
||||||
|
|
||||||
|
val stringFeatures = Map[String, String](
|
||||||
|
"tweetLanguage" -> cand.categoricalFeatures
|
||||||
|
.getOrElse("tweet.core.tweet_text.language", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
FeatureMap(
|
||||||
|
booleanFeatures = booleanFeatures,
|
||||||
|
numericFeatures = continuousFeatures,
|
||||||
|
categoricalFeatures = stringFeatures)
|
||||||
|
}
|
||||||
|
case _ => Future.value(FeatureMap())
|
||||||
|
}
|
||||||
|
case _ => Future.value(FeatureMap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,179 @@
|
|||||||
|
package com.twitter.frigate.pushservice.ml
|
||||||
|
|
||||||
|
import com.twitter.frigate.common.base._
|
||||||
|
import com.twitter.frigate.common.ml.feature.TweetSocialProofKey
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.predicate.quality_model_predicate.PDauCohortUtil
|
||||||
|
import com.twitter.nrel.hydration.base.FeatureInput
|
||||||
|
import com.twitter.nrel.hydration.push.HydrationContext
|
||||||
|
import com.twitter.nrel.hydration.frigate.{FeatureInputs => FI}
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
object HydrationContextBuilder {
|
||||||
|
|
||||||
|
private def getRecUserInputs(
|
||||||
|
pushCandidate: PushCandidate
|
||||||
|
): Set[FI.RecUser] = {
|
||||||
|
pushCandidate match {
|
||||||
|
case userCandidate: UserCandidate =>
|
||||||
|
Set(FI.RecUser(userCandidate.userId))
|
||||||
|
case _ => Set.empty
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getRecTweetInputs(
|
||||||
|
pushCandidate: PushCandidate
|
||||||
|
): Set[FI.RecTweet] =
|
||||||
|
pushCandidate match {
|
||||||
|
case tweetCandidateWithAuthor: TweetCandidate with TweetAuthor with TweetAuthorDetails =>
|
||||||
|
val authorIdOpt = tweetCandidateWithAuthor.authorId
|
||||||
|
Set(FI.RecTweet(tweetCandidateWithAuthor.tweetId, authorIdOpt))
|
||||||
|
case _ => Set.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getMediaInputs(
|
||||||
|
pushCandidate: PushCandidate
|
||||||
|
): Set[FI.Media] =
|
||||||
|
pushCandidate match {
|
||||||
|
case tweetCandidateWithMedia: TweetCandidate with TweetDetails =>
|
||||||
|
tweetCandidateWithMedia.mediaKeys
|
||||||
|
.map { mk =>
|
||||||
|
Set(FI.Media(mk))
|
||||||
|
}.getOrElse(Set.empty)
|
||||||
|
case _ => Set.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getEventInputs(
|
||||||
|
pushCandidate: PushCandidate
|
||||||
|
): Set[FI.Event] = pushCandidate match {
|
||||||
|
case mrEventCandidate: EventCandidate =>
|
||||||
|
Set(FI.Event(mrEventCandidate.eventId))
|
||||||
|
case mfEventCandidate: MagicFanoutEventCandidate =>
|
||||||
|
Set(FI.Event(mfEventCandidate.eventId))
|
||||||
|
case _ => Set.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getTopicInputs(
|
||||||
|
pushCandidate: PushCandidate
|
||||||
|
): Set[FI.Topic] =
|
||||||
|
pushCandidate match {
|
||||||
|
case mrTopicCandidate: TopicCandidate =>
|
||||||
|
mrTopicCandidate.semanticCoreEntityId match {
|
||||||
|
case Some(topicId) => Set(FI.Topic(topicId))
|
||||||
|
case _ => Set.empty
|
||||||
|
}
|
||||||
|
case _ => Set.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getTweetSocialProofKey(
|
||||||
|
pushCandidate: PushCandidate
|
||||||
|
): Future[Set[FI.SocialProofKey]] = {
|
||||||
|
pushCandidate match {
|
||||||
|
case candidate: TweetCandidate with SocialContextActions =>
|
||||||
|
val target = pushCandidate.target
|
||||||
|
target.seedsWithWeight.map { seedsWithWeightOpt =>
|
||||||
|
Set(
|
||||||
|
FI.SocialProofKey(
|
||||||
|
TweetSocialProofKey(
|
||||||
|
seedsWithWeightOpt.getOrElse(Map.empty),
|
||||||
|
candidate.socialContextAllTypeActions
|
||||||
|
))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case _ => Future.value(Set.empty)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getSocialContextInputs(
|
||||||
|
pushCandidate: PushCandidate
|
||||||
|
): Future[Set[FeatureInput]] =
|
||||||
|
pushCandidate match {
|
||||||
|
case candidateWithSC: Candidate with SocialContextActions =>
|
||||||
|
val tweetSocialProofKeyFut = getTweetSocialProofKey(pushCandidate)
|
||||||
|
tweetSocialProofKeyFut.map { tweetSocialProofKeyOpt =>
|
||||||
|
val socialContextUsers = FI.SocialContextUsers(candidateWithSC.socialContextUserIds.toSet)
|
||||||
|
val socialContextActions =
|
||||||
|
FI.SocialContextActions(candidateWithSC.socialContextAllTypeActions)
|
||||||
|
val socialProofKeyOpt = tweetSocialProofKeyOpt
|
||||||
|
Set(Set(socialContextUsers), Set(socialContextActions), socialProofKeyOpt).flatten
|
||||||
|
}
|
||||||
|
case _ => Future.value(Set.empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getPushStringGroupInputs(
|
||||||
|
pushCandidate: PushCandidate
|
||||||
|
): Set[FI.PushStringGroup] =
|
||||||
|
Set(
|
||||||
|
FI.PushStringGroup(
|
||||||
|
pushCandidate.getPushCopy.flatMap(_.pushStringGroup).map(_.toString).getOrElse("")
|
||||||
|
))
|
||||||
|
|
||||||
|
private def getCRTInputs(
|
||||||
|
pushCandidate: PushCandidate
|
||||||
|
): Set[FI.CommonRecommendationType] =
|
||||||
|
Set(FI.CommonRecommendationType(pushCandidate.commonRecType))
|
||||||
|
|
||||||
|
private def getFrigateNotification(
|
||||||
|
pushCandidate: PushCandidate
|
||||||
|
): Set[FI.CandidateFrigateNotification] =
|
||||||
|
Set(FI.CandidateFrigateNotification(pushCandidate.frigateNotification))
|
||||||
|
|
||||||
|
private def getCopyId(
|
||||||
|
pushCandidate: PushCandidate
|
||||||
|
): Set[FI.CopyId] =
|
||||||
|
Set(FI.CopyId(pushCandidate.pushCopyId, pushCandidate.ntabCopyId))
|
||||||
|
|
||||||
|
def build(candidate: PushCandidate): Future[HydrationContext] = {
|
||||||
|
val socialContextInputsFut = getSocialContextInputs(candidate)
|
||||||
|
socialContextInputsFut.map { socialContextInputs =>
|
||||||
|
val featureInputs: Set[FeatureInput] =
|
||||||
|
socialContextInputs ++
|
||||||
|
getRecUserInputs(candidate) ++
|
||||||
|
getRecTweetInputs(candidate) ++
|
||||||
|
getEventInputs(candidate) ++
|
||||||
|
getTopicInputs(candidate) ++
|
||||||
|
getCRTInputs(candidate) ++
|
||||||
|
getPushStringGroupInputs(candidate) ++
|
||||||
|
getMediaInputs(candidate) ++
|
||||||
|
getFrigateNotification(candidate) ++
|
||||||
|
getCopyId(candidate)
|
||||||
|
|
||||||
|
HydrationContext(
|
||||||
|
candidate.target.targetId,
|
||||||
|
featureInputs
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def build(target: Target): Future[HydrationContext] = {
|
||||||
|
val realGraphFeaturesFut = target.realGraphFeatures
|
||||||
|
for {
|
||||||
|
realGraphFeaturesOpt <- realGraphFeaturesFut
|
||||||
|
dauProb <- PDauCohortUtil.getDauProb(target)
|
||||||
|
mrUserStateOpt <- target.targetMrUserState
|
||||||
|
historyInputOpt <-
|
||||||
|
if (target.params(PushFeatureSwitchParams.EnableHydratingOnlineMRHistoryFeatures)) {
|
||||||
|
target.onlineLabeledPushRecs.map { mrHistoryValueOpt =>
|
||||||
|
mrHistoryValueOpt.map(FI.MrHistory)
|
||||||
|
}
|
||||||
|
} else Future.None
|
||||||
|
} yield {
|
||||||
|
val realGraphFeaturesInputOpt = realGraphFeaturesOpt.map { realGraphFeatures =>
|
||||||
|
FI.TargetRealGraphFeatures(realGraphFeatures)
|
||||||
|
}
|
||||||
|
val dauProbInput = FI.DauProb(dauProb)
|
||||||
|
val mrUserStateInput = FI.MrUserState(mrUserStateOpt.map(_.name).getOrElse("unknown"))
|
||||||
|
HydrationContext(
|
||||||
|
target.targetId,
|
||||||
|
Seq(
|
||||||
|
realGraphFeaturesInputOpt,
|
||||||
|
historyInputOpt,
|
||||||
|
Some(dauProbInput),
|
||||||
|
Some(mrUserStateInput)
|
||||||
|
).flatten.toSet
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,188 @@
|
|||||||
|
package com.twitter.frigate.pushservice.ml
|
||||||
|
|
||||||
|
import com.twitter.cortex.deepbird.thriftjava.ModelSelector
|
||||||
|
import com.twitter.finagle.stats.Counter
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.CandidateDetails
|
||||||
|
import com.twitter.frigate.common.base.FeatureMap
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.params.PushMLModel
|
||||||
|
import com.twitter.frigate.pushservice.params.PushModelName
|
||||||
|
import com.twitter.frigate.pushservice.params.WeightedOpenOrNtabClickModel
|
||||||
|
import com.twitter.nrel.heavyranker.PushCandidateHydrationContextWithModel
|
||||||
|
import com.twitter.nrel.heavyranker.PushPredictionServiceStore
|
||||||
|
import com.twitter.nrel.heavyranker.TargetFeatureMapWithModel
|
||||||
|
import com.twitter.timelines.configapi.FSParam
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PushMLModelScorer scores the Candidates and populates their ML scores
|
||||||
|
*
|
||||||
|
* @param pushMLModel Enum to specify which model to use for scoring the Candidates
|
||||||
|
* @param modelToPredictionServiceStoreMap Supports all other prediction services. Specifies model ID -> dbv2 ReadableStore
|
||||||
|
* @param defaultDBv2PredictionServiceStore: Supports models that are not specified in the previous maps (which will be directly configured in the config repo)
|
||||||
|
* @param scoringStats StatsReceiver for scoping stats
|
||||||
|
*/
|
||||||
|
class PushMLModelScorer(
|
||||||
|
pushMLModel: PushMLModel.Value,
|
||||||
|
modelToPredictionServiceStoreMap: Map[
|
||||||
|
WeightedOpenOrNtabClickModel.ModelNameType,
|
||||||
|
PushPredictionServiceStore
|
||||||
|
],
|
||||||
|
defaultDBv2PredictionServiceStore: PushPredictionServiceStore,
|
||||||
|
scoringStats: StatsReceiver) {
|
||||||
|
|
||||||
|
val queriesOutsideTheModelMaps: StatsReceiver =
|
||||||
|
scoringStats.scope("queries_outside_the_model_maps")
|
||||||
|
val totalQueriesOutsideTheModelMaps: Counter =
|
||||||
|
queriesOutsideTheModelMaps.counter("total")
|
||||||
|
|
||||||
|
private def scoreByBatchPredictionForModelFromMultiModelService(
|
||||||
|
predictionServiceStore: PushPredictionServiceStore,
|
||||||
|
modelVersion: WeightedOpenOrNtabClickModel.ModelNameType,
|
||||||
|
candidatesDetails: Seq[CandidateDetails[PushCandidate]],
|
||||||
|
useCommonFeatures: Boolean,
|
||||||
|
overridePushMLModel: PushMLModel.Value
|
||||||
|
): Seq[CandidateDetails[PushCandidate]] = {
|
||||||
|
val modelName =
|
||||||
|
PushModelName(overridePushMLModel, modelVersion).toString
|
||||||
|
val modelSelector = new ModelSelector()
|
||||||
|
modelSelector.setId(modelName)
|
||||||
|
|
||||||
|
val candidateHydrationWithFeaturesMap = candidatesDetails.map { candidatesDetail =>
|
||||||
|
(
|
||||||
|
candidatesDetail.candidate.candidateHydrationContext,
|
||||||
|
candidatesDetail.candidate.candidateFeatureMap())
|
||||||
|
}
|
||||||
|
if (candidatesDetails.nonEmpty) {
|
||||||
|
val candidatesWithScore = predictionServiceStore.getBatchPredictionsForModel(
|
||||||
|
candidatesDetails.head.candidate.target.targetHydrationContext,
|
||||||
|
candidatesDetails.head.candidate.target.featureMap,
|
||||||
|
candidateHydrationWithFeaturesMap,
|
||||||
|
Some(modelSelector),
|
||||||
|
useCommonFeatures
|
||||||
|
)
|
||||||
|
candidatesDetails.zip(candidatesWithScore).foreach {
|
||||||
|
case (candidateDetail, (_, scoreOptFut)) =>
|
||||||
|
candidateDetail.candidate.populateQualityModelScore(
|
||||||
|
overridePushMLModel,
|
||||||
|
modelVersion,
|
||||||
|
scoreOptFut
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
candidatesDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
private def scoreByBatchPrediction(
|
||||||
|
modelVersion: WeightedOpenOrNtabClickModel.ModelNameType,
|
||||||
|
candidatesDetails: Seq[CandidateDetails[PushCandidate]],
|
||||||
|
useCommonFeaturesForDBv2Service: Boolean,
|
||||||
|
overridePushMLModel: PushMLModel.Value
|
||||||
|
): Seq[CandidateDetails[PushCandidate]] = {
|
||||||
|
if (modelToPredictionServiceStoreMap.contains(modelVersion)) {
|
||||||
|
scoreByBatchPredictionForModelFromMultiModelService(
|
||||||
|
modelToPredictionServiceStoreMap(modelVersion),
|
||||||
|
modelVersion,
|
||||||
|
candidatesDetails,
|
||||||
|
useCommonFeaturesForDBv2Service,
|
||||||
|
overridePushMLModel
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
totalQueriesOutsideTheModelMaps.incr()
|
||||||
|
queriesOutsideTheModelMaps.counter(modelVersion).incr()
|
||||||
|
scoreByBatchPredictionForModelFromMultiModelService(
|
||||||
|
defaultDBv2PredictionServiceStore,
|
||||||
|
modelVersion,
|
||||||
|
candidatesDetails,
|
||||||
|
useCommonFeaturesForDBv2Service,
|
||||||
|
overridePushMLModel
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def scoreByBatchPredictionForModelVersion(
|
||||||
|
target: Target,
|
||||||
|
candidatesDetails: Seq[CandidateDetails[PushCandidate]],
|
||||||
|
modelVersionParam: FSParam[WeightedOpenOrNtabClickModel.ModelNameType],
|
||||||
|
useCommonFeaturesForDBv2Service: Boolean = true,
|
||||||
|
overridePushMLModelOpt: Option[PushMLModel.Value] = None
|
||||||
|
): Seq[CandidateDetails[PushCandidate]] = {
|
||||||
|
scoreByBatchPrediction(
|
||||||
|
target.params(modelVersionParam),
|
||||||
|
candidatesDetails,
|
||||||
|
useCommonFeaturesForDBv2Service,
|
||||||
|
overridePushMLModelOpt.getOrElse(pushMLModel)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def singlePredicationForModelVersion(
|
||||||
|
modelVersion: String,
|
||||||
|
candidate: PushCandidate,
|
||||||
|
overridePushMLModelOpt: Option[PushMLModel.Value] = None
|
||||||
|
): Future[Option[Double]] = {
|
||||||
|
val modelSelector = new ModelSelector()
|
||||||
|
modelSelector.setId(
|
||||||
|
PushModelName(overridePushMLModelOpt.getOrElse(pushMLModel), modelVersion).toString
|
||||||
|
)
|
||||||
|
if (modelToPredictionServiceStoreMap.contains(modelVersion)) {
|
||||||
|
modelToPredictionServiceStoreMap(modelVersion).get(
|
||||||
|
PushCandidateHydrationContextWithModel(
|
||||||
|
candidate.target.targetHydrationContext,
|
||||||
|
candidate.target.featureMap,
|
||||||
|
candidate.candidateHydrationContext,
|
||||||
|
candidate.candidateFeatureMap(),
|
||||||
|
Some(modelSelector)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
totalQueriesOutsideTheModelMaps.incr()
|
||||||
|
queriesOutsideTheModelMaps.counter(modelVersion).incr()
|
||||||
|
defaultDBv2PredictionServiceStore.get(
|
||||||
|
PushCandidateHydrationContextWithModel(
|
||||||
|
candidate.target.targetHydrationContext,
|
||||||
|
candidate.target.featureMap,
|
||||||
|
candidate.candidateHydrationContext,
|
||||||
|
candidate.candidateFeatureMap(),
|
||||||
|
Some(modelSelector)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def singlePredictionForTargetLevel(
|
||||||
|
modelVersion: String,
|
||||||
|
targetId: Long,
|
||||||
|
featureMap: Future[FeatureMap]
|
||||||
|
): Future[Option[Double]] = {
|
||||||
|
val modelSelector = new ModelSelector()
|
||||||
|
modelSelector.setId(
|
||||||
|
PushModelName(pushMLModel, modelVersion).toString
|
||||||
|
)
|
||||||
|
defaultDBv2PredictionServiceStore.getForTargetLevel(
|
||||||
|
TargetFeatureMapWithModel(targetId, featureMap, Some(modelSelector))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getScoreHistogramCounters(
|
||||||
|
stats: StatsReceiver,
|
||||||
|
scopeName: String,
|
||||||
|
histogramBinSize: Double
|
||||||
|
): IndexedSeq[Counter] = {
|
||||||
|
val histogramScopedStatsReceiver = stats.scope(scopeName)
|
||||||
|
val numBins = math.ceil(1.0 / histogramBinSize).toInt
|
||||||
|
|
||||||
|
(0 to numBins) map { k =>
|
||||||
|
if (k == 0)
|
||||||
|
histogramScopedStatsReceiver.counter("candidates_with_scores_zero")
|
||||||
|
else {
|
||||||
|
val counterName = "candidates_with_scores_from_%s_to_%s".format(
|
||||||
|
"%.2f".format(histogramBinSize * (k - 1)).replace(".", ""),
|
||||||
|
"%.2f".format(math.min(1.0, histogramBinSize * k)).replace(".", ""))
|
||||||
|
histogramScopedStatsReceiver.counter(counterName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,89 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.DiscoverTwitterCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.DiscoverTwitterPushIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.DiscoverTwitterNtabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.predicate.PredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicRFPHPredicates
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.OutOfNetworkTweetPredicates
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.hermit.predicate.NamedPredicate
|
||||||
|
|
||||||
|
class DiscoverTwitterPushCandidate(
|
||||||
|
candidate: RawCandidate with DiscoverTwitterCandidate,
|
||||||
|
copyIds: CopyIds,
|
||||||
|
)(
|
||||||
|
implicit val statsScoped: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with DiscoverTwitterCandidate
|
||||||
|
with DiscoverTwitterPushIbis2Hydrator
|
||||||
|
with DiscoverTwitterNtabRequestHydrator {
|
||||||
|
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
|
||||||
|
override val target: Target = candidate.target
|
||||||
|
|
||||||
|
override lazy val commonRecType: CommonRecommendationType = candidate.commonRecType
|
||||||
|
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
|
||||||
|
override val statsReceiver: StatsReceiver =
|
||||||
|
statsScoped.scope("DiscoverTwitterPushCandidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
case class AddressBookPushCandidatePredicates(config: Config)
|
||||||
|
extends BasicRFPHPredicates[DiscoverTwitterPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val predicates: List[
|
||||||
|
NamedPredicate[DiscoverTwitterPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
PredicatesForCandidate.paramPredicate(
|
||||||
|
PushFeatureSwitchParams.EnableAddressBookPush
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class CompleteOnboardingPushCandidatePredicates(config: Config)
|
||||||
|
extends BasicRFPHPredicates[DiscoverTwitterPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val predicates: List[
|
||||||
|
NamedPredicate[DiscoverTwitterPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
PredicatesForCandidate.paramPredicate(
|
||||||
|
PushFeatureSwitchParams.EnableCompleteOnboardingPush
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class PopGeoTweetCandidatePredicates(override val config: Config)
|
||||||
|
extends OutOfNetworkTweetPredicates[OutOfNetworkTweetPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override def postCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[OutOfNetworkTweetPushCandidate]
|
||||||
|
] = List(
|
||||||
|
PredicatesForCandidate.htlFatiguePredicate(
|
||||||
|
PushFeatureSwitchParams.NewUserPlaybookAllowedLastLoginHours
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.F1FirstDegree
|
||||||
|
import com.twitter.frigate.common.base.SocialContextAction
|
||||||
|
import com.twitter.frigate.common.base.SocialGraphServiceRelationshipMap
|
||||||
|
import com.twitter.frigate.common.base.TweetAuthorDetails
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes._
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.F1FirstDegreeTweetIbis2HydratorForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.F1FirstDegreeTweetNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicTweetPredicatesForRFPHWithoutSGSPredicates
|
||||||
|
import com.twitter.frigate.pushservice.util.CandidateHydrationUtil.TweetWithSocialContextTraits
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.hermit.predicate.socialgraph.RelationEdge
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class F1TweetPushCandidate(
|
||||||
|
candidate: RawCandidate with TweetWithSocialContextTraits,
|
||||||
|
author: Future[Option[User]],
|
||||||
|
socialGraphServiceResultMap: Map[RelationEdge, Boolean],
|
||||||
|
copyIds: CopyIds
|
||||||
|
)(
|
||||||
|
implicit stats: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with F1FirstDegree
|
||||||
|
with TweetAuthorDetails
|
||||||
|
with SocialGraphServiceRelationshipMap
|
||||||
|
with F1FirstDegreeTweetNTabRequestHydrator
|
||||||
|
with F1FirstDegreeTweetIbis2HydratorForCandidate {
|
||||||
|
override val socialContextActions: Seq[SocialContextAction] =
|
||||||
|
candidate.socialContextActions
|
||||||
|
override val socialContextAllTypeActions: Seq[SocialContextAction] =
|
||||||
|
candidate.socialContextActions
|
||||||
|
override val statsReceiver: StatsReceiver = stats
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
override val tweetId: Long = candidate.tweetId
|
||||||
|
override lazy val tweetyPieResult: Option[TweetyPie.TweetyPieResult] =
|
||||||
|
candidate.tweetyPieResult
|
||||||
|
override lazy val tweetAuthor: Future[Option[User]] = author
|
||||||
|
override val target: PushTypes.Target = candidate.target
|
||||||
|
override lazy val commonRecType: CommonRecommendationType =
|
||||||
|
candidate.commonRecType
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
|
||||||
|
override val relationshipMap: Map[RelationEdge, Boolean] = socialGraphServiceResultMap
|
||||||
|
}
|
||||||
|
|
||||||
|
case class F1TweetCandidatePredicates(override val config: Config)
|
||||||
|
extends BasicTweetPredicatesForRFPHWithoutSGSPredicates[F1TweetPushCandidate] {
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
}
|
@ -0,0 +1,72 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.channels.common.thriftscala.ApiList
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.ListPushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.ListIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.ListCandidateNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.predicate.ListPredicates
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicRFPHPredicates
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.hermit.predicate.NamedPredicate
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class ListRecommendationPushCandidate(
|
||||||
|
val apiListStore: ReadableStore[Long, ApiList],
|
||||||
|
candidate: RawCandidate with ListPushCandidate,
|
||||||
|
copyIds: CopyIds
|
||||||
|
)(
|
||||||
|
implicit stats: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with ListPushCandidate
|
||||||
|
with ListIbis2Hydrator
|
||||||
|
with ListCandidateNTabRequestHydrator {
|
||||||
|
|
||||||
|
override val commonRecType: CommonRecommendationType = candidate.commonRecType
|
||||||
|
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
|
||||||
|
override val statsReceiver: StatsReceiver = stats
|
||||||
|
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
|
||||||
|
override val target: PushTypes.Target = candidate.target
|
||||||
|
|
||||||
|
override val listId: Long = candidate.listId
|
||||||
|
|
||||||
|
lazy val apiList: Future[Option[ApiList]] = apiListStore.get(listId)
|
||||||
|
|
||||||
|
lazy val listName: Future[Option[String]] = apiList.map { apiListOpt =>
|
||||||
|
apiListOpt.map(_.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val listOwnerId: Future[Option[Long]] = apiList.map { apiListOpt =>
|
||||||
|
apiListOpt.map(_.ownerId)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case class ListRecommendationPredicates(config: Config)
|
||||||
|
extends BasicRFPHPredicates[ListRecommendationPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val predicates: List[NamedPredicate[ListRecommendationPushCandidate]] = List(
|
||||||
|
ListPredicates.listNameExistsPredicate(),
|
||||||
|
ListPredicates.listAuthorExistsPredicate(),
|
||||||
|
ListPredicates.listAuthorAcceptableToTargetUser(config.edgeStore),
|
||||||
|
ListPredicates.listAcceptablePredicate(),
|
||||||
|
ListPredicates.listSubscriberCountPredicate()
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,136 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.HydratedMagicFanoutCreatorEventCandidate
|
||||||
|
import com.twitter.frigate.common.base.MagicFanoutCreatorEventCandidate
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.CreatorFanoutType
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.MagicEventsReason
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.MagicFanoutCreatorEventIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.MagicFanoutCreatorEventNtabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.predicate.PredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.magic_fanout.MagicFanoutPredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.ntab_caret_fatigue.MagicFanoutNtabCaretFatiguePredicate
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicSendHandlerPredicates
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.hermit.predicate.NamedPredicate
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.strato.client.UserId
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import scala.util.control.NoStackTrace
|
||||||
|
|
||||||
|
class MagicFanoutCreatorEventPushCandidateHydratorException(private val message: String)
|
||||||
|
extends Exception(message)
|
||||||
|
with NoStackTrace
|
||||||
|
|
||||||
|
class MagicFanoutCreatorEventPushCandidate(
|
||||||
|
candidate: RawCandidate with MagicFanoutCreatorEventCandidate,
|
||||||
|
creatorUser: Option[User],
|
||||||
|
copyIds: CopyIds,
|
||||||
|
creatorTweetCountStore: ReadableStore[UserId, Int]
|
||||||
|
)(
|
||||||
|
implicit val statsScoped: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with MagicFanoutCreatorEventIbis2Hydrator
|
||||||
|
with MagicFanoutCreatorEventNtabRequestHydrator
|
||||||
|
with MagicFanoutCreatorEventCandidate
|
||||||
|
with HydratedMagicFanoutCreatorEventCandidate {
|
||||||
|
override def creatorId: Long = candidate.creatorId
|
||||||
|
|
||||||
|
override def hydratedCreator: Option[User] = creatorUser
|
||||||
|
|
||||||
|
override lazy val numberOfTweetsFut: Future[Option[Int]] =
|
||||||
|
creatorTweetCountStore.get(UserId(creatorId))
|
||||||
|
|
||||||
|
lazy val userProfile = hydratedCreator
|
||||||
|
.flatMap(_.profile).getOrElse(
|
||||||
|
throw new MagicFanoutCreatorEventPushCandidateHydratorException(
|
||||||
|
s"Unable to get user profile to generate tapThrough for userId: $creatorId"))
|
||||||
|
|
||||||
|
override val frigateNotification: FrigateNotification = candidate.frigateNotification
|
||||||
|
|
||||||
|
override def subscriberId: Option[Long] = candidate.subscriberId
|
||||||
|
|
||||||
|
override def creatorFanoutType: CreatorFanoutType = candidate.creatorFanoutType
|
||||||
|
|
||||||
|
override def target: PushTypes.Target = candidate.target
|
||||||
|
|
||||||
|
override def pushId: Long = candidate.pushId
|
||||||
|
|
||||||
|
override def candidateMagicEventsReasons: Seq[MagicEventsReason] =
|
||||||
|
candidate.candidateMagicEventsReasons
|
||||||
|
|
||||||
|
override def statsReceiver: StatsReceiver = statsScoped
|
||||||
|
|
||||||
|
override def pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
|
||||||
|
override def ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
|
||||||
|
override def copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
|
||||||
|
override def commonRecType: CommonRecommendationType = candidate.commonRecType
|
||||||
|
|
||||||
|
override def weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case class MagicFanouCreatorSubscriptionEventPushPredicates(config: Config)
|
||||||
|
extends BasicSendHandlerPredicates[MagicFanoutCreatorEventPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val preCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[MagicFanoutCreatorEventPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
PredicatesForCandidate.paramPredicate(
|
||||||
|
PushFeatureSwitchParams.EnableCreatorSubscriptionPush
|
||||||
|
),
|
||||||
|
PredicatesForCandidate.isDeviceEligibleForCreatorPush,
|
||||||
|
MagicFanoutPredicatesForCandidate.creatorPushTargetIsNotCreator(),
|
||||||
|
MagicFanoutPredicatesForCandidate.duplicateCreatorPredicate,
|
||||||
|
MagicFanoutPredicatesForCandidate.magicFanoutCreatorPushFatiguePredicate(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override val postCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[MagicFanoutCreatorEventPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
MagicFanoutNtabCaretFatiguePredicate(),
|
||||||
|
MagicFanoutPredicatesForCandidate.isSuperFollowingCreator()(config, statsReceiver).flip
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
case class MagicFanoutNewCreatorEventPushPredicates(config: Config)
|
||||||
|
extends BasicSendHandlerPredicates[MagicFanoutCreatorEventPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val preCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[MagicFanoutCreatorEventPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
PredicatesForCandidate.paramPredicate(
|
||||||
|
PushFeatureSwitchParams.EnableNewCreatorPush
|
||||||
|
),
|
||||||
|
PredicatesForCandidate.isDeviceEligibleForCreatorPush,
|
||||||
|
MagicFanoutPredicatesForCandidate.duplicateCreatorPredicate,
|
||||||
|
MagicFanoutPredicatesForCandidate.magicFanoutCreatorPushFatiguePredicate,
|
||||||
|
)
|
||||||
|
|
||||||
|
override val postCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[MagicFanoutCreatorEventPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
MagicFanoutNtabCaretFatiguePredicate(),
|
||||||
|
MagicFanoutPredicatesForCandidate.isSuperFollowingCreator()(config, statsReceiver).flip
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,303 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.escherbird.metadata.thriftscala.EntityMegadata
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.MagicFanoutEventCandidate
|
||||||
|
import com.twitter.frigate.common.base.RecommendationType
|
||||||
|
import com.twitter.frigate.common.store.interests.InterestsLookupRequestWithContext
|
||||||
|
import com.twitter.frigate.common.util.HighPriorityLocaleUtil
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.FanoutEvent
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.FanoutMetadata
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.MagicEventsReason
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.NewsForYouMetadata
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.ReasonSource
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.TargetID
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.Ibis2HydratorForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.EventNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.predicate.magic_fanout.MagicFanoutPredicatesUtil
|
||||||
|
import com.twitter.frigate.pushservice.store.EventRequest
|
||||||
|
import com.twitter.frigate.pushservice.store.UttEntityHydrationStore
|
||||||
|
import com.twitter.frigate.pushservice.util.PushDeviceUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.TopicsUtil
|
||||||
|
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||||
|
import com.twitter.frigate.thriftscala.MagicFanoutEventNotificationDetails
|
||||||
|
import com.twitter.hermit.store.semantic_core.SemanticEntityForQuery
|
||||||
|
import com.twitter.interests.thriftscala.InterestId.SemanticCore
|
||||||
|
import com.twitter.interests.thriftscala.UserInterests
|
||||||
|
import com.twitter.livevideo.common.ids.CountryId
|
||||||
|
import com.twitter.livevideo.common.ids.UserId
|
||||||
|
import com.twitter.livevideo.timeline.domain.v2.Event
|
||||||
|
import com.twitter.livevideo.timeline.domain.v2.HydrationOptions
|
||||||
|
import com.twitter.livevideo.timeline.domain.v2.LookupContext
|
||||||
|
import com.twitter.simclusters_v2.thriftscala.SimClustersInferredEntities
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.topiclisting.utt.LocalizedEntity
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
abstract class MagicFanoutEventPushCandidate(
|
||||||
|
candidate: RawCandidate with MagicFanoutEventCandidate with RecommendationType,
|
||||||
|
copyIds: CopyIds,
|
||||||
|
override val fanoutEvent: Option[FanoutEvent],
|
||||||
|
override val semanticEntityResults: Map[SemanticEntityForQuery, Option[EntityMegadata]],
|
||||||
|
simClusterToEntities: Map[Int, Option[SimClustersInferredEntities]],
|
||||||
|
lexServiceStore: ReadableStore[EventRequest, Event],
|
||||||
|
interestsLookupStore: ReadableStore[InterestsLookupRequestWithContext, UserInterests],
|
||||||
|
uttEntityHydrationStore: UttEntityHydrationStore
|
||||||
|
)(
|
||||||
|
implicit statsScoped: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with MagicFanoutEventHydratedCandidate
|
||||||
|
with MagicFanoutEventCandidate
|
||||||
|
with EventNTabRequestHydrator
|
||||||
|
with RecommendationType
|
||||||
|
with Ibis2HydratorForCandidate {
|
||||||
|
|
||||||
|
override lazy val eventFut: Future[Option[Event]] = {
|
||||||
|
eventRequestFut.flatMap {
|
||||||
|
case Some(eventRequest) => lexServiceStore.get(eventRequest)
|
||||||
|
case _ => Future.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override val frigateNotification: FrigateNotification = candidate.frigateNotification
|
||||||
|
|
||||||
|
override val pushId: Long = candidate.pushId
|
||||||
|
|
||||||
|
override val candidateMagicEventsReasons: Seq[MagicEventsReason] =
|
||||||
|
candidate.candidateMagicEventsReasons
|
||||||
|
|
||||||
|
override val eventId: Long = candidate.eventId
|
||||||
|
|
||||||
|
override val momentId: Option[Long] = candidate.momentId
|
||||||
|
|
||||||
|
override val target: Target = candidate.target
|
||||||
|
|
||||||
|
override val eventLanguage: Option[String] = candidate.eventLanguage
|
||||||
|
|
||||||
|
override val details: Option[MagicFanoutEventNotificationDetails] = candidate.details
|
||||||
|
|
||||||
|
override lazy val stats: StatsReceiver = statsScoped.scope("MagicFanoutEventPushCandidate")
|
||||||
|
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
|
||||||
|
override val statsReceiver: StatsReceiver = statsScoped.scope("MagicFanoutEventPushCandidate")
|
||||||
|
|
||||||
|
override val effectiveMagicEventsReasons: Option[Seq[MagicEventsReason]] = Some(
|
||||||
|
candidateMagicEventsReasons)
|
||||||
|
|
||||||
|
lazy val newsForYouMetadata: Option[NewsForYouMetadata] =
|
||||||
|
fanoutEvent.flatMap { event =>
|
||||||
|
{
|
||||||
|
event.fanoutMetadata.collect {
|
||||||
|
case FanoutMetadata.NewsForYouMetadata(nfyMetadata) => nfyMetadata
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val reverseIndexedTopicIds = candidate.candidateMagicEventsReasons
|
||||||
|
.filter(_.source.contains(ReasonSource.UttTopicFollowGraph))
|
||||||
|
.map(_.reason).collect {
|
||||||
|
case TargetID.SemanticCoreID(semanticCoreID) => semanticCoreID.entityId
|
||||||
|
}.toSet
|
||||||
|
|
||||||
|
val ergSemanticCoreIds = candidate.candidateMagicEventsReasons
|
||||||
|
.filter(_.source.contains(ReasonSource.ErgShortTermInterestSemanticCore)).map(
|
||||||
|
_.reason).collect {
|
||||||
|
case TargetID.SemanticCoreID(semanticCoreID) => semanticCoreID.entityId
|
||||||
|
}.toSet
|
||||||
|
|
||||||
|
override lazy val ergLocalizedEntities = TopicsUtil
|
||||||
|
.getLocalizedEntityMap(target, ergSemanticCoreIds, uttEntityHydrationStore)
|
||||||
|
.map { localizedEntityMap =>
|
||||||
|
ergSemanticCoreIds.collect {
|
||||||
|
case topicId if localizedEntityMap.contains(topicId) => localizedEntityMap(topicId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventSemanticCoreEntityIds: Seq[Long] = {
|
||||||
|
val entityIds = for {
|
||||||
|
event <- fanoutEvent
|
||||||
|
targets <- event.targets
|
||||||
|
} yield {
|
||||||
|
targets.flatMap {
|
||||||
|
_.whitelist.map {
|
||||||
|
_.collect {
|
||||||
|
case TargetID.SemanticCoreID(semanticCoreID) => semanticCoreID.entityId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
entityIds.map(_.flatten).getOrElse(Seq.empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
val eventSemanticCoreDomainIds: Seq[Long] = {
|
||||||
|
val domainIds = for {
|
||||||
|
event <- fanoutEvent
|
||||||
|
targets <- event.targets
|
||||||
|
} yield {
|
||||||
|
targets.flatMap {
|
||||||
|
_.whitelist.map {
|
||||||
|
_.collect {
|
||||||
|
case TargetID.SemanticCoreID(semanticCoreID) => semanticCoreID.domainId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
domainIds.map(_.flatten).getOrElse(Seq.empty)
|
||||||
|
}
|
||||||
|
|
||||||
|
override lazy val followedTopicLocalizedEntities: Future[Set[LocalizedEntity]] = {
|
||||||
|
|
||||||
|
val isNewSignupTargetingReason = candidateMagicEventsReasons.size == 1 &&
|
||||||
|
candidateMagicEventsReasons.headOption.exists(_.source.contains(ReasonSource.NewSignup))
|
||||||
|
|
||||||
|
val shouldFetchTopicFollows = reverseIndexedTopicIds.nonEmpty || isNewSignupTargetingReason
|
||||||
|
|
||||||
|
val topicFollows = if (shouldFetchTopicFollows) {
|
||||||
|
TopicsUtil
|
||||||
|
.getTopicsFollowedByUser(
|
||||||
|
candidate.target,
|
||||||
|
interestsLookupStore,
|
||||||
|
stats.stat("followed_topics")
|
||||||
|
).map { _.getOrElse(Seq.empty) }.map {
|
||||||
|
_.flatMap {
|
||||||
|
_.interestId match {
|
||||||
|
case SemanticCore(semanticCore) => Some(semanticCore.id)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else Future.Nil
|
||||||
|
|
||||||
|
topicFollows.flatMap { followedTopicIds =>
|
||||||
|
val topicIds = if (isNewSignupTargetingReason) {
|
||||||
|
// if new signup is the only targeting reason then we check the event targeting reason
|
||||||
|
// against realtime topic follows.
|
||||||
|
eventSemanticCoreEntityIds.toSet.intersect(followedTopicIds.toSet)
|
||||||
|
} else {
|
||||||
|
// check against the fanout reason of topics
|
||||||
|
followedTopicIds.toSet.intersect(reverseIndexedTopicIds)
|
||||||
|
}
|
||||||
|
|
||||||
|
TopicsUtil
|
||||||
|
.getLocalizedEntityMap(target, topicIds, uttEntityHydrationStore)
|
||||||
|
.map { localizedEntityMap =>
|
||||||
|
topicIds.collect {
|
||||||
|
case topicId if localizedEntityMap.contains(topicId) => localizedEntityMap(topicId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val simClusterToEntityMapping: Map[Int, Seq[Long]] =
|
||||||
|
simClusterToEntities.flatMap {
|
||||||
|
case (clusterId, Some(inferredEntities)) =>
|
||||||
|
statsReceiver.counter("with_cluster_to_entity_mapping").incr()
|
||||||
|
Some(
|
||||||
|
(
|
||||||
|
clusterId,
|
||||||
|
inferredEntities.entities
|
||||||
|
.map(_.entityId)))
|
||||||
|
case _ =>
|
||||||
|
statsReceiver.counter("without_cluster_to_entity_mapping").incr()
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val annotatedAndInferredSemanticCoreEntities: Seq[Long] =
|
||||||
|
(simClusterToEntityMapping, eventFanoutReasonEntities) match {
|
||||||
|
case (entityMapping, eventFanoutReasons) =>
|
||||||
|
entityMapping.values.flatten.toSeq ++
|
||||||
|
eventFanoutReasons.semanticCoreIds.map(_.entityId)
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val shouldHydrateSquareImage = target.deviceInfo.map { deviceInfo =>
|
||||||
|
(PushDeviceUtil.isPrimaryDeviceIOS(deviceInfo) &&
|
||||||
|
target.params(PushFeatureSwitchParams.EnableEventSquareMediaIosMagicFanoutNewsEvent)) ||
|
||||||
|
(PushDeviceUtil.isPrimaryDeviceAndroid(deviceInfo) &&
|
||||||
|
target.params(PushFeatureSwitchParams.EnableEventSquareMediaAndroid))
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val shouldHydratePrimaryImage: Future[Boolean] = target.deviceInfo.map { deviceInfo =>
|
||||||
|
(PushDeviceUtil.isPrimaryDeviceAndroid(deviceInfo) &&
|
||||||
|
target.params(PushFeatureSwitchParams.EnableEventPrimaryMediaAndroid))
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val eventRequestFut: Future[Option[EventRequest]] =
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
target.inferredUserDeviceLanguage,
|
||||||
|
target.accountCountryCode,
|
||||||
|
shouldHydrateSquareImage,
|
||||||
|
shouldHydratePrimaryImage).map {
|
||||||
|
case (
|
||||||
|
inferredUserDeviceLanguage,
|
||||||
|
accountCountryCode,
|
||||||
|
shouldHydrateSquareImage,
|
||||||
|
shouldHydratePrimaryImage) =>
|
||||||
|
if (shouldHydrateSquareImage || shouldHydratePrimaryImage) {
|
||||||
|
Some(
|
||||||
|
EventRequest(
|
||||||
|
eventId,
|
||||||
|
lookupContext = LookupContext(
|
||||||
|
hydrationOptions = HydrationOptions(
|
||||||
|
includeSquareImage = shouldHydrateSquareImage,
|
||||||
|
includePrimaryImage = shouldHydratePrimaryImage
|
||||||
|
),
|
||||||
|
language = inferredUserDeviceLanguage,
|
||||||
|
countryCode = accountCountryCode,
|
||||||
|
userId = Some(UserId(target.targetId))
|
||||||
|
)
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Some(
|
||||||
|
EventRequest(
|
||||||
|
eventId,
|
||||||
|
lookupContext = LookupContext(
|
||||||
|
language = inferredUserDeviceLanguage,
|
||||||
|
countryCode = accountCountryCode
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val isHighPriorityEvent: Future[Boolean] = target.accountCountryCode.map { countryCodeOpt =>
|
||||||
|
val isHighPriorityPushOpt = for {
|
||||||
|
countryCode <- countryCodeOpt
|
||||||
|
nfyMetadata <- newsForYouMetadata
|
||||||
|
eventContext <- nfyMetadata.eventContextScribe
|
||||||
|
} yield {
|
||||||
|
val highPriorityLocales = HighPriorityLocaleUtil.getHighPriorityLocales(
|
||||||
|
eventContext = eventContext,
|
||||||
|
defaultLocalesOpt = nfyMetadata.locales)
|
||||||
|
val highPriorityGeos = HighPriorityLocaleUtil.getHighPriorityGeos(
|
||||||
|
eventContext = eventContext,
|
||||||
|
defaultGeoPlaceIdsOpt = nfyMetadata.placeIds)
|
||||||
|
val isHighPriorityLocalePush =
|
||||||
|
highPriorityLocales.flatMap(_.country).map(CountryId(_)).contains(CountryId(countryCode))
|
||||||
|
val isHighPriorityGeoPush = MagicFanoutPredicatesUtil
|
||||||
|
.geoPlaceIdsFromReasons(candidateMagicEventsReasons)
|
||||||
|
.intersect(highPriorityGeos.toSet)
|
||||||
|
.nonEmpty
|
||||||
|
stats.scope("is_high_priority_locale_push").counter(s"$isHighPriorityLocalePush").incr()
|
||||||
|
stats.scope("is_high_priority_geo_push").counter(s"$isHighPriorityGeoPush").incr()
|
||||||
|
isHighPriorityLocalePush || isHighPriorityGeoPush
|
||||||
|
}
|
||||||
|
isHighPriorityPushOpt.getOrElse(false)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,147 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.escherbird.common.thriftscala.QualifiedId
|
||||||
|
import com.twitter.escherbird.metadata.thriftscala.BasicMetadata
|
||||||
|
import com.twitter.escherbird.metadata.thriftscala.EntityIndexFields
|
||||||
|
import com.twitter.escherbird.metadata.thriftscala.EntityMegadata
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.MagicFanoutCandidate
|
||||||
|
import com.twitter.frigate.common.base.MagicFanoutEventCandidate
|
||||||
|
import com.twitter.frigate.common.base.RichEventFutCandidate
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.AnnotationAlg
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.FanoutEvent
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.MagicEventsReason
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.SemanticCoreID
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.SimClusterID
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.TargetID
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.hermit.store.semantic_core.SemanticEntityForQuery
|
||||||
|
import com.twitter.livevideo.timeline.domain.v2.Event
|
||||||
|
import com.twitter.topiclisting.utt.LocalizedEntity
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
case class FanoutReasonEntities(
|
||||||
|
userIds: Set[Long],
|
||||||
|
placeIds: Set[Long],
|
||||||
|
semanticCoreIds: Set[SemanticCoreID],
|
||||||
|
simclusterIds: Set[SimClusterID]) {
|
||||||
|
val qualifiedIds: Set[QualifiedId] =
|
||||||
|
semanticCoreIds.map(e => QualifiedId(e.domainId, e.entityId))
|
||||||
|
}
|
||||||
|
|
||||||
|
object FanoutReasonEntities {
|
||||||
|
val empty = FanoutReasonEntities(
|
||||||
|
userIds = Set.empty,
|
||||||
|
placeIds = Set.empty,
|
||||||
|
semanticCoreIds = Set.empty,
|
||||||
|
simclusterIds = Set.empty
|
||||||
|
)
|
||||||
|
|
||||||
|
def from(reasons: Seq[TargetID]): FanoutReasonEntities = {
|
||||||
|
val userIds: Set[Long] = reasons.collect {
|
||||||
|
case TargetID.UserID(userId) => userId.id
|
||||||
|
}.toSet
|
||||||
|
val placeIds: Set[Long] = reasons.collect {
|
||||||
|
case TargetID.PlaceID(placeId) => placeId.id
|
||||||
|
}.toSet
|
||||||
|
val semanticCoreIds: Set[SemanticCoreID] = reasons.collect {
|
||||||
|
case TargetID.SemanticCoreID(semanticCoreID) => semanticCoreID
|
||||||
|
}.toSet
|
||||||
|
val simclusterIds: Set[SimClusterID] = reasons.collect {
|
||||||
|
case TargetID.SimClusterID(simClusterID) => simClusterID
|
||||||
|
}.toSet
|
||||||
|
|
||||||
|
FanoutReasonEntities(
|
||||||
|
userIds = userIds,
|
||||||
|
placeIds,
|
||||||
|
semanticCoreIds = semanticCoreIds,
|
||||||
|
simclusterIds = simclusterIds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait MagicFanoutHydratedCandidate extends PushCandidate with MagicFanoutCandidate {
|
||||||
|
lazy val fanoutReasonEntities: FanoutReasonEntities =
|
||||||
|
FanoutReasonEntities.from(candidateMagicEventsReasons.map(_.reason))
|
||||||
|
}
|
||||||
|
|
||||||
|
trait MagicFanoutEventHydratedCandidate
|
||||||
|
extends MagicFanoutHydratedCandidate
|
||||||
|
with MagicFanoutEventCandidate
|
||||||
|
with RichEventFutCandidate {
|
||||||
|
|
||||||
|
def target: PushTypes.Target
|
||||||
|
|
||||||
|
def stats: StatsReceiver
|
||||||
|
|
||||||
|
def fanoutEvent: Option[FanoutEvent]
|
||||||
|
|
||||||
|
def eventFut: Future[Option[Event]]
|
||||||
|
|
||||||
|
def semanticEntityResults: Map[SemanticEntityForQuery, Option[EntityMegadata]]
|
||||||
|
|
||||||
|
def effectiveMagicEventsReasons: Option[Seq[MagicEventsReason]]
|
||||||
|
|
||||||
|
def followedTopicLocalizedEntities: Future[Set[LocalizedEntity]]
|
||||||
|
|
||||||
|
def ergLocalizedEntities: Future[Set[LocalizedEntity]]
|
||||||
|
|
||||||
|
lazy val entityAnnotationAlg: Map[TargetID, Set[AnnotationAlg]] =
|
||||||
|
fanoutEvent
|
||||||
|
.flatMap { metadata =>
|
||||||
|
metadata.eventAnnotationInfo.map { eventAnnotationInfo =>
|
||||||
|
eventAnnotationInfo.map {
|
||||||
|
case (target, annotationInfoSet) => target -> annotationInfoSet.map(_.alg).toSet
|
||||||
|
}.toMap
|
||||||
|
}
|
||||||
|
}.getOrElse(Map.empty)
|
||||||
|
|
||||||
|
lazy val eventSource: Option[String] = fanoutEvent.map { metadata =>
|
||||||
|
val source = metadata.eventSource.getOrElse("undefined")
|
||||||
|
stats.scope("eventSource").counter(source).incr()
|
||||||
|
source
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val semanticCoreEntityTags: Map[(Long, Long), Set[String]] =
|
||||||
|
semanticEntityResults.flatMap {
|
||||||
|
case (semanticEntityForQuery, entityMegadataOpt: Option[EntityMegadata]) =>
|
||||||
|
for {
|
||||||
|
entityMegadata <- entityMegadataOpt
|
||||||
|
basicMetadata: BasicMetadata <- entityMegadata.basicMetadata
|
||||||
|
indexableFields: EntityIndexFields <- basicMetadata.indexableFields
|
||||||
|
tags <- indexableFields.tags
|
||||||
|
} yield {
|
||||||
|
((semanticEntityForQuery.domainId, semanticEntityForQuery.entityId), tags.toSet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val owningTwitterUserIds: Seq[Long] = semanticEntityResults.values.flatten
|
||||||
|
.flatMap {
|
||||||
|
_.basicMetadata.flatMap(_.twitter.flatMap(_.owningTwitterUserIds))
|
||||||
|
}.flatten
|
||||||
|
.toSeq
|
||||||
|
.distinct
|
||||||
|
|
||||||
|
lazy val eventFanoutReasonEntities: FanoutReasonEntities =
|
||||||
|
fanoutEvent match {
|
||||||
|
case Some(fanout) =>
|
||||||
|
fanout.targets
|
||||||
|
.map { targets: Seq[thriftscala.Target] =>
|
||||||
|
FanoutReasonEntities.from(targets.flatMap(_.whitelist).flatten)
|
||||||
|
}.getOrElse(FanoutReasonEntities.empty)
|
||||||
|
case _ => FanoutReasonEntities.empty
|
||||||
|
}
|
||||||
|
|
||||||
|
override lazy val eventResultFut: Future[Event] = eventFut.map {
|
||||||
|
case Some(eventResult) => eventResult
|
||||||
|
case _ =>
|
||||||
|
throw new IllegalArgumentException("event is None for MagicFanoutEventHydratedCandidate")
|
||||||
|
}
|
||||||
|
override val rankScore: Option[Double] = None
|
||||||
|
override val predictionScore: Option[Double] = None
|
||||||
|
}
|
||||||
|
|
||||||
|
case class MagicFanoutEventHydratedInfo(
|
||||||
|
fanoutEvent: Option[FanoutEvent],
|
||||||
|
semanticEntityResults: Map[SemanticEntityForQuery, Option[EntityMegadata]])
|
@ -0,0 +1,99 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.escherbird.metadata.thriftscala.EntityMegadata
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.MagicFanoutNewsEventCandidate
|
||||||
|
import com.twitter.frigate.common.store.interests.InterestsLookupRequestWithContext
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.FanoutEvent
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.MagicFanoutNewsEventIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.MagicFanoutNewsEventNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.predicate.PredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.event.EventPredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.magic_fanout.MagicFanoutPredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.magic_fanout.MagicFanoutTargetingPredicateWrappersForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.ntab_caret_fatigue.MagicFanoutNtabCaretFatiguePredicate
|
||||||
|
import com.twitter.frigate.pushservice.store.EventRequest
|
||||||
|
import com.twitter.frigate.pushservice.store.UttEntityHydrationStore
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicSendHandlerPredicates
|
||||||
|
import com.twitter.hermit.predicate.NamedPredicate
|
||||||
|
import com.twitter.hermit.store.semantic_core.SemanticEntityForQuery
|
||||||
|
import com.twitter.interests.thriftscala.UserInterests
|
||||||
|
import com.twitter.livevideo.timeline.domain.v2.Event
|
||||||
|
import com.twitter.simclusters_v2.thriftscala.SimClustersInferredEntities
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
|
||||||
|
class MagicFanoutNewsEventPushCandidate(
|
||||||
|
candidate: RawCandidate with MagicFanoutNewsEventCandidate,
|
||||||
|
copyIds: CopyIds,
|
||||||
|
override val fanoutEvent: Option[FanoutEvent],
|
||||||
|
override val semanticEntityResults: Map[SemanticEntityForQuery, Option[EntityMegadata]],
|
||||||
|
simClusterToEntities: Map[Int, Option[SimClustersInferredEntities]],
|
||||||
|
lexServiceStore: ReadableStore[EventRequest, Event],
|
||||||
|
interestsLookupStore: ReadableStore[InterestsLookupRequestWithContext, UserInterests],
|
||||||
|
uttEntityHydrationStore: UttEntityHydrationStore
|
||||||
|
)(
|
||||||
|
implicit statsScoped: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends MagicFanoutEventPushCandidate(
|
||||||
|
candidate,
|
||||||
|
copyIds,
|
||||||
|
fanoutEvent,
|
||||||
|
semanticEntityResults,
|
||||||
|
simClusterToEntities,
|
||||||
|
lexServiceStore,
|
||||||
|
interestsLookupStore,
|
||||||
|
uttEntityHydrationStore
|
||||||
|
)(statsScoped, pushModelScorer)
|
||||||
|
with MagicFanoutNewsEventCandidate
|
||||||
|
with MagicFanoutNewsEventIbis2Hydrator
|
||||||
|
with MagicFanoutNewsEventNTabRequestHydrator {
|
||||||
|
|
||||||
|
override lazy val stats: StatsReceiver = statsScoped.scope("MagicFanoutNewsEventPushCandidate")
|
||||||
|
override val statsReceiver: StatsReceiver = statsScoped.scope("MagicFanoutNewsEventPushCandidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
case class MagicFanoutNewsEventCandidatePredicates(config: Config)
|
||||||
|
extends BasicSendHandlerPredicates[MagicFanoutNewsEventPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val preCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[MagicFanoutNewsEventPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
EventPredicatesForCandidate.accountCountryPredicateWithAllowlist,
|
||||||
|
PredicatesForCandidate.isDeviceEligibleForNewsOrSports,
|
||||||
|
MagicFanoutPredicatesForCandidate.inferredUserDeviceLanguagePredicate,
|
||||||
|
PredicatesForCandidate.secondaryDormantAccountPredicate(statsReceiver),
|
||||||
|
MagicFanoutPredicatesForCandidate.highPriorityNewsEventExceptedPredicate(
|
||||||
|
MagicFanoutTargetingPredicateWrappersForCandidate
|
||||||
|
.magicFanoutTargetingPredicate(statsReceiver, config)
|
||||||
|
)(config),
|
||||||
|
MagicFanoutPredicatesForCandidate.geoOptOutPredicate(config.safeUserStore),
|
||||||
|
EventPredicatesForCandidate.isNotDuplicateWithEventId,
|
||||||
|
MagicFanoutPredicatesForCandidate.highPriorityNewsEventExceptedPredicate(
|
||||||
|
MagicFanoutPredicatesForCandidate.newsNotificationFatigue()
|
||||||
|
)(config),
|
||||||
|
MagicFanoutPredicatesForCandidate.highPriorityNewsEventExceptedPredicate(
|
||||||
|
MagicFanoutNtabCaretFatiguePredicate()
|
||||||
|
)(config),
|
||||||
|
MagicFanoutPredicatesForCandidate.escherbirdMagicfanoutEventParam()(statsReceiver),
|
||||||
|
MagicFanoutPredicatesForCandidate.hasCustomTargetingForNewsEventsParam(
|
||||||
|
statsReceiver
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
override val postCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[MagicFanoutNewsEventPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
MagicFanoutPredicatesForCandidate.magicFanoutNoOptoutInterestPredicate,
|
||||||
|
MagicFanoutPredicatesForCandidate.geoTargetingHoldback(),
|
||||||
|
MagicFanoutPredicatesForCandidate.userGeneratedEventsPredicate,
|
||||||
|
EventPredicatesForCandidate.hasTitle,
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,95 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.MagicFanoutProductLaunchCandidate
|
||||||
|
import com.twitter.frigate.common.util.{FeatureSwitchParams => FS}
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.MagicEventsReason
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.ProductType
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.predicate.magic_fanout.MagicFanoutPredicatesUtil
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.MagicFanoutProductLaunchIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.MagicFanoutProductLaunchNtabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.predicate.PredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.magic_fanout.MagicFanoutPredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.ntab_caret_fatigue.MagicFanoutNtabCaretFatiguePredicate
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicSendHandlerPredicates
|
||||||
|
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||||
|
import com.twitter.hermit.predicate.NamedPredicate
|
||||||
|
|
||||||
|
class MagicFanoutProductLaunchPushCandidate(
|
||||||
|
candidate: RawCandidate with MagicFanoutProductLaunchCandidate,
|
||||||
|
copyIds: CopyIds
|
||||||
|
)(
|
||||||
|
implicit val statsScoped: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with MagicFanoutProductLaunchCandidate
|
||||||
|
with MagicFanoutProductLaunchIbis2Hydrator
|
||||||
|
with MagicFanoutProductLaunchNtabRequestHydrator {
|
||||||
|
|
||||||
|
override val frigateNotification: FrigateNotification = candidate.frigateNotification
|
||||||
|
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
|
||||||
|
override val pushId: Long = candidate.pushId
|
||||||
|
|
||||||
|
override val productLaunchType: ProductType = candidate.productLaunchType
|
||||||
|
|
||||||
|
override val candidateMagicEventsReasons: Seq[MagicEventsReason] =
|
||||||
|
candidate.candidateMagicEventsReasons
|
||||||
|
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
|
||||||
|
override val target: Target = candidate.target
|
||||||
|
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
|
||||||
|
override val statsReceiver: StatsReceiver =
|
||||||
|
statsScoped.scope("MagicFanoutProductLaunchPushCandidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
case class MagicFanoutProductLaunchPushCandidatePredicates(config: Config)
|
||||||
|
extends BasicSendHandlerPredicates[MagicFanoutProductLaunchPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val preCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[MagicFanoutProductLaunchPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
PredicatesForCandidate.isDeviceEligibleForCreatorPush,
|
||||||
|
PredicatesForCandidate.exceptedPredicate(
|
||||||
|
"excepted_is_target_blue_verified",
|
||||||
|
MagicFanoutPredicatesUtil.shouldSkipBlueVerifiedCheckForCandidate,
|
||||||
|
PredicatesForCandidate.isTargetBlueVerified.flip
|
||||||
|
), // no need to send if target is already Blue Verified
|
||||||
|
PredicatesForCandidate.exceptedPredicate(
|
||||||
|
"excepted_is_target_legacy_verified",
|
||||||
|
MagicFanoutPredicatesUtil.shouldSkipLegacyVerifiedCheckForCandidate,
|
||||||
|
PredicatesForCandidate.isTargetLegacyVerified.flip
|
||||||
|
), // no need to send if target is already Legacy Verified
|
||||||
|
PredicatesForCandidate.exceptedPredicate(
|
||||||
|
"excepted_is_target_super_follow_creator",
|
||||||
|
MagicFanoutPredicatesUtil.shouldSkipSuperFollowCreatorCheckForCandidate,
|
||||||
|
PredicatesForCandidate.isTargetSuperFollowCreator.flip
|
||||||
|
), // no need to send if target is already Super Follow Creator
|
||||||
|
PredicatesForCandidate.paramPredicate(
|
||||||
|
FS.EnableMagicFanoutProductLaunch
|
||||||
|
),
|
||||||
|
MagicFanoutPredicatesForCandidate.magicFanoutProductLaunchFatigue(),
|
||||||
|
)
|
||||||
|
|
||||||
|
override val postCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[MagicFanoutProductLaunchPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
MagicFanoutNtabCaretFatiguePredicate(),
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,119 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.escherbird.metadata.thriftscala.EntityMegadata
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.BaseGameScore
|
||||||
|
import com.twitter.frigate.common.base.MagicFanoutSportsEventCandidate
|
||||||
|
import com.twitter.frigate.common.base.MagicFanoutSportsScoreInformation
|
||||||
|
import com.twitter.frigate.common.base.TeamInfo
|
||||||
|
import com.twitter.frigate.common.store.interests.InterestsLookupRequestWithContext
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.FanoutEvent
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.MagicFanoutSportsEventIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.MagicFanoutSportsEventNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.predicate.PredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.magic_fanout.MagicFanoutPredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.magic_fanout.MagicFanoutTargetingPredicateWrappersForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.ntab_caret_fatigue.MagicFanoutNtabCaretFatiguePredicate
|
||||||
|
import com.twitter.frigate.pushservice.store.EventRequest
|
||||||
|
import com.twitter.frigate.pushservice.store.UttEntityHydrationStore
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicSendHandlerPredicates
|
||||||
|
import com.twitter.hermit.predicate.NamedPredicate
|
||||||
|
import com.twitter.hermit.store.semantic_core.SemanticEntityForQuery
|
||||||
|
import com.twitter.interests.thriftscala.UserInterests
|
||||||
|
import com.twitter.livevideo.timeline.domain.v2.Event
|
||||||
|
import com.twitter.livevideo.timeline.domain.v2.HydrationOptions
|
||||||
|
import com.twitter.livevideo.timeline.domain.v2.LookupContext
|
||||||
|
import com.twitter.simclusters_v2.thriftscala.SimClustersInferredEntities
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class MagicFanoutSportsPushCandidate(
|
||||||
|
candidate: RawCandidate
|
||||||
|
with MagicFanoutSportsEventCandidate
|
||||||
|
with MagicFanoutSportsScoreInformation,
|
||||||
|
copyIds: CopyIds,
|
||||||
|
override val fanoutEvent: Option[FanoutEvent],
|
||||||
|
override val semanticEntityResults: Map[SemanticEntityForQuery, Option[EntityMegadata]],
|
||||||
|
simClusterToEntities: Map[Int, Option[SimClustersInferredEntities]],
|
||||||
|
lexServiceStore: ReadableStore[EventRequest, Event],
|
||||||
|
interestsLookupStore: ReadableStore[InterestsLookupRequestWithContext, UserInterests],
|
||||||
|
uttEntityHydrationStore: UttEntityHydrationStore
|
||||||
|
)(
|
||||||
|
implicit statsScoped: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends MagicFanoutEventPushCandidate(
|
||||||
|
candidate,
|
||||||
|
copyIds,
|
||||||
|
fanoutEvent,
|
||||||
|
semanticEntityResults,
|
||||||
|
simClusterToEntities,
|
||||||
|
lexServiceStore,
|
||||||
|
interestsLookupStore,
|
||||||
|
uttEntityHydrationStore)(statsScoped, pushModelScorer)
|
||||||
|
with MagicFanoutSportsEventCandidate
|
||||||
|
with MagicFanoutSportsScoreInformation
|
||||||
|
with MagicFanoutSportsEventNTabRequestHydrator
|
||||||
|
with MagicFanoutSportsEventIbis2Hydrator {
|
||||||
|
|
||||||
|
override val isScoreUpdate: Boolean = candidate.isScoreUpdate
|
||||||
|
override val gameScores: Future[Option[BaseGameScore]] = candidate.gameScores
|
||||||
|
override val homeTeamInfo: Future[Option[TeamInfo]] = candidate.homeTeamInfo
|
||||||
|
override val awayTeamInfo: Future[Option[TeamInfo]] = candidate.awayTeamInfo
|
||||||
|
|
||||||
|
override lazy val stats: StatsReceiver = statsScoped.scope("MagicFanoutSportsPushCandidate")
|
||||||
|
override val statsReceiver: StatsReceiver = statsScoped.scope("MagicFanoutSportsPushCandidate")
|
||||||
|
|
||||||
|
override lazy val eventRequestFut: Future[Option[EventRequest]] = {
|
||||||
|
Future.join(target.inferredUserDeviceLanguage, target.accountCountryCode).map {
|
||||||
|
case (inferredUserDeviceLanguage, accountCountryCode) =>
|
||||||
|
Some(
|
||||||
|
EventRequest(
|
||||||
|
eventId,
|
||||||
|
lookupContext = LookupContext(
|
||||||
|
hydrationOptions = HydrationOptions(
|
||||||
|
includeSquareImage = true,
|
||||||
|
includePrimaryImage = true
|
||||||
|
),
|
||||||
|
language = inferredUserDeviceLanguage,
|
||||||
|
countryCode = accountCountryCode
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case class MagicFanoutSportsEventCandidatePredicates(config: Config)
|
||||||
|
extends BasicSendHandlerPredicates[MagicFanoutSportsPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val preCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[MagicFanoutSportsPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
PredicatesForCandidate.paramPredicate(PushFeatureSwitchParams.EnableScoreFanoutNotification)
|
||||||
|
)
|
||||||
|
|
||||||
|
override val postCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[MagicFanoutSportsPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
PredicatesForCandidate.isDeviceEligibleForNewsOrSports,
|
||||||
|
MagicFanoutPredicatesForCandidate.inferredUserDeviceLanguagePredicate,
|
||||||
|
MagicFanoutPredicatesForCandidate.highPriorityEventExceptedPredicate(
|
||||||
|
MagicFanoutTargetingPredicateWrappersForCandidate
|
||||||
|
.magicFanoutTargetingPredicate(statsReceiver, config)
|
||||||
|
)(config),
|
||||||
|
PredicatesForCandidate.secondaryDormantAccountPredicate(
|
||||||
|
statsReceiver
|
||||||
|
),
|
||||||
|
MagicFanoutPredicatesForCandidate.highPriorityEventExceptedPredicate(
|
||||||
|
MagicFanoutNtabCaretFatiguePredicate()
|
||||||
|
)(config),
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,68 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.contentrecommender.thriftscala.MetricTag
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.OutOfNetworkTweetCandidate
|
||||||
|
import com.twitter.frigate.common.base.TopicCandidate
|
||||||
|
import com.twitter.frigate.common.base.TweetAuthorDetails
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.OutOfNetworkTweetIbis2HydratorForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.OutOfNetworkTweetNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.predicate.HealthPredicates
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.OutOfNetworkTweetPredicates
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.hermit.predicate.NamedPredicate
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie
|
||||||
|
import com.twitter.topiclisting.utt.LocalizedEntity
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class OutOfNetworkTweetPushCandidate(
|
||||||
|
candidate: RawCandidate with OutOfNetworkTweetCandidate with TopicCandidate,
|
||||||
|
author: Future[Option[User]],
|
||||||
|
copyIds: CopyIds
|
||||||
|
)(
|
||||||
|
implicit stats: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with OutOfNetworkTweetCandidate
|
||||||
|
with TopicCandidate
|
||||||
|
with TweetAuthorDetails
|
||||||
|
with OutOfNetworkTweetNTabRequestHydrator
|
||||||
|
with OutOfNetworkTweetIbis2HydratorForCandidate {
|
||||||
|
override val statsReceiver: StatsReceiver = stats
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
override val tweetId: Long = candidate.tweetId
|
||||||
|
override lazy val tweetyPieResult: Option[TweetyPie.TweetyPieResult] =
|
||||||
|
candidate.tweetyPieResult
|
||||||
|
override lazy val tweetAuthor: Future[Option[User]] = author
|
||||||
|
override val target: PushTypes.Target = candidate.target
|
||||||
|
override lazy val commonRecType: CommonRecommendationType =
|
||||||
|
candidate.commonRecType
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
override lazy val semanticCoreEntityId: Option[Long] = candidate.semanticCoreEntityId
|
||||||
|
override lazy val localizedUttEntity: Option[LocalizedEntity] = candidate.localizedUttEntity
|
||||||
|
override lazy val algorithmCR: Option[String] = candidate.algorithmCR
|
||||||
|
override lazy val isMrBackfillCR: Option[Boolean] = candidate.isMrBackfillCR
|
||||||
|
override lazy val tagsCR: Option[Seq[MetricTag]] = candidate.tagsCR
|
||||||
|
}
|
||||||
|
|
||||||
|
case class OutOfNetworkTweetCandidatePredicates(override val config: Config)
|
||||||
|
extends OutOfNetworkTweetPredicates[OutOfNetworkTweetPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override def postCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[OutOfNetworkTweetPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
HealthPredicates.agathaAbusiveTweetAuthorPredicateMrTwistly(),
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.frigate.common.base._
|
||||||
|
import com.twitter.frigate.common.candidate.UserLanguage
|
||||||
|
import com.twitter.frigate.common.candidate._
|
||||||
|
import com.twitter.frigate.data_pipeline.features_common.RequestContextForFeatureStore
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyInfo
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.MLScores
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.QualityScribing
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.Scriber
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.Ibis2HydratorForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.NTabRequest
|
||||||
|
import com.twitter.frigate.pushservice.take.ChannelForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.target._
|
||||||
|
import com.twitter.util.Time
|
||||||
|
|
||||||
|
object PushTypes {
|
||||||
|
|
||||||
|
trait Target
|
||||||
|
extends TargetUser
|
||||||
|
with UserDetails
|
||||||
|
with TargetWithPushContext
|
||||||
|
with TargetDecider
|
||||||
|
with TargetABDecider
|
||||||
|
with FrigateHistory
|
||||||
|
with PushTargeting
|
||||||
|
with TargetScoringDetails
|
||||||
|
with TweetImpressionHistory
|
||||||
|
with CustomConfigForExpt
|
||||||
|
with CaretFeedbackHistory
|
||||||
|
with NotificationFeedbackHistory
|
||||||
|
with PromptFeedbackHistory
|
||||||
|
with HTLVisitHistory
|
||||||
|
with MaxTweetAge
|
||||||
|
with NewUserDetails
|
||||||
|
with ResurrectedUserDetails
|
||||||
|
with TargetWithSeedUsers
|
||||||
|
with MagicFanoutHistory
|
||||||
|
with OptOutUserInterests
|
||||||
|
with RequestContextForFeatureStore
|
||||||
|
with TargetAppPermissions
|
||||||
|
with UserLanguage
|
||||||
|
with InlineActionHistory
|
||||||
|
with TargetPlaces
|
||||||
|
|
||||||
|
trait RawCandidate extends Candidate with TargetInfo[PushTypes.Target] with RecommendationType {
|
||||||
|
|
||||||
|
val createdAt: Time = Time.now
|
||||||
|
}
|
||||||
|
|
||||||
|
trait PushCandidate
|
||||||
|
extends RawCandidate
|
||||||
|
with CandidateScoringDetails
|
||||||
|
with MLScores
|
||||||
|
with QualityScribing
|
||||||
|
with CopyInfo
|
||||||
|
with Scriber
|
||||||
|
with Ibis2HydratorForCandidate
|
||||||
|
with NTabRequest
|
||||||
|
with ChannelForCandidate
|
||||||
|
}
|
@ -0,0 +1,85 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.ScheduledSpaceSpeakerCandidate
|
||||||
|
import com.twitter.frigate.common.base.SpaceCandidateFanoutDetails
|
||||||
|
import com.twitter.frigate.common.util.FeatureSwitchParams
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.SpaceMetadata
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.ScheduledSpaceSpeakerIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.ScheduledSpaceSpeakerNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.predicate.PredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.predicate.SpacePredicate
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicSendHandlerPredicates
|
||||||
|
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.hermit.predicate.NamedPredicate
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.ubs.thriftscala.AudioSpace
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class ScheduledSpaceSpeakerPushCandidate(
|
||||||
|
candidate: RawCandidate with ScheduledSpaceSpeakerCandidate,
|
||||||
|
hostUser: Option[User],
|
||||||
|
copyIds: CopyIds,
|
||||||
|
audioSpaceStore: ReadableStore[String, AudioSpace]
|
||||||
|
)(
|
||||||
|
implicit val statsScoped: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with ScheduledSpaceSpeakerCandidate
|
||||||
|
with ScheduledSpaceSpeakerIbis2Hydrator
|
||||||
|
with SpaceCandidateFanoutDetails
|
||||||
|
with ScheduledSpaceSpeakerNTabRequestHydrator {
|
||||||
|
|
||||||
|
override val startTime: Long = candidate.startTime
|
||||||
|
|
||||||
|
override val hydratedHost: Option[User] = hostUser
|
||||||
|
|
||||||
|
override val spaceId: String = candidate.spaceId
|
||||||
|
|
||||||
|
override val hostId: Option[Long] = candidate.hostId
|
||||||
|
|
||||||
|
override val speakerIds: Option[Seq[Long]] = candidate.speakerIds
|
||||||
|
|
||||||
|
override val listenerIds: Option[Seq[Long]] = candidate.listenerIds
|
||||||
|
|
||||||
|
override val frigateNotification: FrigateNotification = candidate.frigateNotification
|
||||||
|
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
|
||||||
|
override val target: Target = candidate.target
|
||||||
|
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
|
||||||
|
override lazy val audioSpaceFut: Future[Option[AudioSpace]] = audioSpaceStore.get(spaceId)
|
||||||
|
|
||||||
|
override val spaceFanoutMetadata: Option[SpaceMetadata] = None
|
||||||
|
|
||||||
|
override val statsReceiver: StatsReceiver =
|
||||||
|
statsScoped.scope("ScheduledSpaceSpeakerCandidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
case class ScheduledSpaceSpeakerCandidatePredicates(config: Config)
|
||||||
|
extends BasicSendHandlerPredicates[ScheduledSpaceSpeakerPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val preCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[ScheduledSpaceSpeakerPushCandidate]
|
||||||
|
] = List(
|
||||||
|
SpacePredicate.scheduledSpaceStarted(
|
||||||
|
config.audioSpaceStore
|
||||||
|
),
|
||||||
|
PredicatesForCandidate.paramPredicate(FeatureSwitchParams.EnableScheduledSpaceSpeakers)
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,86 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.ScheduledSpaceSubscriberCandidate
|
||||||
|
import com.twitter.frigate.common.base.SpaceCandidateFanoutDetails
|
||||||
|
import com.twitter.frigate.common.util.FeatureSwitchParams
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.SpaceMetadata
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.Target
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.ScheduledSpaceSubscriberIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.ScheduledSpaceSubscriberNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.predicate._
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicSendHandlerPredicates
|
||||||
|
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.hermit.predicate.NamedPredicate
|
||||||
|
import com.twitter.storehaus.ReadableStore
|
||||||
|
import com.twitter.ubs.thriftscala.AudioSpace
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class ScheduledSpaceSubscriberPushCandidate(
|
||||||
|
candidate: RawCandidate with ScheduledSpaceSubscriberCandidate,
|
||||||
|
hostUser: Option[User],
|
||||||
|
copyIds: CopyIds,
|
||||||
|
audioSpaceStore: ReadableStore[String, AudioSpace]
|
||||||
|
)(
|
||||||
|
implicit val statsScoped: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with ScheduledSpaceSubscriberCandidate
|
||||||
|
with SpaceCandidateFanoutDetails
|
||||||
|
with ScheduledSpaceSubscriberIbis2Hydrator
|
||||||
|
with ScheduledSpaceSubscriberNTabRequestHydrator {
|
||||||
|
|
||||||
|
override val startTime: Long = candidate.startTime
|
||||||
|
|
||||||
|
override val hydratedHost: Option[User] = hostUser
|
||||||
|
|
||||||
|
override val spaceId: String = candidate.spaceId
|
||||||
|
|
||||||
|
override val hostId: Option[Long] = candidate.hostId
|
||||||
|
|
||||||
|
override val speakerIds: Option[Seq[Long]] = candidate.speakerIds
|
||||||
|
|
||||||
|
override val listenerIds: Option[Seq[Long]] = candidate.listenerIds
|
||||||
|
|
||||||
|
override val frigateNotification: FrigateNotification = candidate.frigateNotification
|
||||||
|
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
|
||||||
|
override val target: Target = candidate.target
|
||||||
|
|
||||||
|
override lazy val audioSpaceFut: Future[Option[AudioSpace]] = audioSpaceStore.get(spaceId)
|
||||||
|
|
||||||
|
override val spaceFanoutMetadata: Option[SpaceMetadata] = None
|
||||||
|
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
|
||||||
|
override val statsReceiver: StatsReceiver =
|
||||||
|
statsScoped.scope("ScheduledSpaceSubscriberCandidate")
|
||||||
|
}
|
||||||
|
|
||||||
|
case class ScheduledSpaceSubscriberCandidatePredicates(config: Config)
|
||||||
|
extends BasicSendHandlerPredicates[ScheduledSpaceSubscriberPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val preCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[ScheduledSpaceSubscriberPushCandidate]
|
||||||
|
] =
|
||||||
|
List(
|
||||||
|
PredicatesForCandidate.paramPredicate(FeatureSwitchParams.EnableScheduledSpaceSubscribers),
|
||||||
|
SpacePredicate.narrowCastSpace,
|
||||||
|
SpacePredicate.targetInSpace(config.audioSpaceParticipantsStore),
|
||||||
|
SpacePredicate.spaceHostTargetUserBlocking(config.edgeStore),
|
||||||
|
PredicatesForCandidate.duplicateSpacesPredicate
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,56 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.SubscribedSearchTweetCandidate
|
||||||
|
import com.twitter.frigate.common.base.TweetAuthorDetails
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.SubscribedSearchTweetIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.SubscribedSearchTweetNtabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicTweetPredicatesForRFPH
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class SubscribedSearchTweetPushCandidate(
|
||||||
|
candidate: RawCandidate with SubscribedSearchTweetCandidate,
|
||||||
|
author: Option[User],
|
||||||
|
copyIds: CopyIds
|
||||||
|
)(
|
||||||
|
implicit stats: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with SubscribedSearchTweetCandidate
|
||||||
|
with TweetAuthorDetails
|
||||||
|
with SubscribedSearchTweetIbis2Hydrator
|
||||||
|
with SubscribedSearchTweetNtabRequestHydrator {
|
||||||
|
override def tweetAuthor: Future[Option[User]] = Future.value(author)
|
||||||
|
|
||||||
|
override def weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
|
||||||
|
override def tweetId: Long = candidate.tweetId
|
||||||
|
|
||||||
|
override def pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
|
||||||
|
override def ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
|
||||||
|
override def copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
|
||||||
|
override def target: PushTypes.Target = candidate.target
|
||||||
|
|
||||||
|
override def searchTerm: String = candidate.searchTerm
|
||||||
|
|
||||||
|
override def timeBoundedLandingUrl: Option[String] = None
|
||||||
|
|
||||||
|
override def statsReceiver: StatsReceiver = stats
|
||||||
|
|
||||||
|
override def tweetyPieResult: Option[TweetyPie.TweetyPieResult] = candidate.tweetyPieResult
|
||||||
|
}
|
||||||
|
|
||||||
|
case class SubscribedSearchTweetCandidatePredicates(override val config: Config)
|
||||||
|
extends BasicTweetPredicatesForRFPH[SubscribedSearchTweetPushCandidate] {
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
}
|
@ -0,0 +1,70 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.TopTweetImpressionsCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.TopTweetImpressionsCandidateIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.TopTweetImpressionsNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.predicate.TopTweetImpressionsPredicates
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicTweetPredicatesForRFPH
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.hermit.predicate.NamedPredicate
|
||||||
|
import com.twitter.notificationservice.thriftscala.StoryContext
|
||||||
|
import com.twitter.notificationservice.thriftscala.StoryContextValue
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class defines a hydrated [[TopTweetImpressionsCandidate]]
|
||||||
|
*
|
||||||
|
* @param candidate: [[TopTweetImpressionsCandidate]] for the candidate representing the user's Tweet with the most impressions
|
||||||
|
* @param copyIds: push and ntab notification copy
|
||||||
|
* @param stats: finagle scoped states receiver
|
||||||
|
* @param pushModelScorer: ML model score object for fetching prediction scores
|
||||||
|
*/
|
||||||
|
class TopTweetImpressionsPushCandidate(
|
||||||
|
candidate: RawCandidate with TopTweetImpressionsCandidate,
|
||||||
|
copyIds: CopyIds
|
||||||
|
)(
|
||||||
|
implicit stats: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with TopTweetImpressionsCandidate
|
||||||
|
with TopTweetImpressionsNTabRequestHydrator
|
||||||
|
with TopTweetImpressionsCandidateIbis2Hydrator {
|
||||||
|
override val target: PushTypes.Target = candidate.target
|
||||||
|
override val commonRecType: CommonRecommendationType = candidate.commonRecType
|
||||||
|
override val tweetId: Long = candidate.tweetId
|
||||||
|
override lazy val tweetyPieResult: Option[TweetyPie.TweetyPieResult] =
|
||||||
|
candidate.tweetyPieResult
|
||||||
|
override val impressionsCount: Long = candidate.impressionsCount
|
||||||
|
|
||||||
|
override val statsReceiver: StatsReceiver = stats.scope(getClass.getSimpleName)
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
override val storyContext: Option[StoryContext] =
|
||||||
|
Some(StoryContext(altText = "", value = Some(StoryContextValue.Tweets(Seq(tweetId)))))
|
||||||
|
}
|
||||||
|
|
||||||
|
case class TopTweetImpressionsPushCandidatePredicates(config: Config)
|
||||||
|
extends BasicTweetPredicatesForRFPH[TopTweetImpressionsPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val preCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[TopTweetImpressionsPushCandidate]
|
||||||
|
] = List(
|
||||||
|
TopTweetImpressionsPredicates.topTweetImpressionsFatiguePredicate
|
||||||
|
)
|
||||||
|
|
||||||
|
override val postCandidateSpecificPredicates: List[
|
||||||
|
NamedPredicate[TopTweetImpressionsPushCandidate]
|
||||||
|
] = List(
|
||||||
|
TopTweetImpressionsPredicates.topTweetImpressionsThreshold()
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,71 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.TopicProofTweetCandidate
|
||||||
|
import com.twitter.frigate.common.base.TweetAuthorDetails
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.TopicProofTweetIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.TopicProofTweetNtabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.predicate.PredicatesForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicTweetPredicatesForRFPH
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.hermit.predicate.NamedPredicate
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class defines a hydrated [[TopicProofTweetCandidate]]
|
||||||
|
*
|
||||||
|
* @param candidate : [[TopicProofTweetCandidate]] for the candidate representint a Tweet recommendation for followed Topic
|
||||||
|
* @param author : Tweet author representated as Gizmoduck user object
|
||||||
|
* @param copyIds : push and ntab notification copy
|
||||||
|
* @param stats : finagle scoped states receiver
|
||||||
|
* @param pushModelScorer : ML model score object for fetching prediction scores
|
||||||
|
*/
|
||||||
|
class TopicProofTweetPushCandidate(
|
||||||
|
candidate: RawCandidate with TopicProofTweetCandidate,
|
||||||
|
author: Option[User],
|
||||||
|
copyIds: CopyIds
|
||||||
|
)(
|
||||||
|
implicit stats: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with TopicProofTweetCandidate
|
||||||
|
with TweetAuthorDetails
|
||||||
|
with TopicProofTweetNtabRequestHydrator
|
||||||
|
with TopicProofTweetIbis2Hydrator {
|
||||||
|
override val statsReceiver: StatsReceiver = stats
|
||||||
|
override val target: PushTypes.Target = candidate.target
|
||||||
|
override val tweetId: Long = candidate.tweetId
|
||||||
|
override lazy val tweetyPieResult: Option[TweetyPie.TweetyPieResult] = candidate.tweetyPieResult
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
override val semanticCoreEntityId = candidate.semanticCoreEntityId
|
||||||
|
override val localizedUttEntity = candidate.localizedUttEntity
|
||||||
|
override val tweetAuthor = Future.value(author)
|
||||||
|
override val topicListingSetting = candidate.topicListingSetting
|
||||||
|
override val algorithmCR = candidate.algorithmCR
|
||||||
|
override val commonRecType: CommonRecommendationType = candidate.commonRecType
|
||||||
|
override val tagsCR = candidate.tagsCR
|
||||||
|
override val isOutOfNetwork = candidate.isOutOfNetwork
|
||||||
|
}
|
||||||
|
|
||||||
|
case class TopicProofTweetCandidatePredicates(override val config: Config)
|
||||||
|
extends BasicTweetPredicatesForRFPH[TopicProofTweetPushCandidate] {
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val preCandidateSpecificPredicates: List[NamedPredicate[TopicProofTweetPushCandidate]] =
|
||||||
|
List(
|
||||||
|
PredicatesForCandidate.paramPredicate(
|
||||||
|
PushFeatureSwitchParams.EnableTopicProofTweetRecs
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.events.recos.thriftscala.TrendsContext
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.TrendTweetCandidate
|
||||||
|
import com.twitter.frigate.common.base.TweetAuthorDetails
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.TrendTweetIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.TrendTweetNtabHydrator
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicTweetPredicatesForRFPH
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class TrendTweetPushCandidate(
|
||||||
|
candidate: RawCandidate with TrendTweetCandidate,
|
||||||
|
author: Option[User],
|
||||||
|
copyIds: CopyIds
|
||||||
|
)(
|
||||||
|
implicit stats: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with TrendTweetCandidate
|
||||||
|
with TweetAuthorDetails
|
||||||
|
with TrendTweetIbis2Hydrator
|
||||||
|
with TrendTweetNtabHydrator {
|
||||||
|
override val statsReceiver: StatsReceiver = stats
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
override val tweetId: Long = candidate.tweetId
|
||||||
|
override lazy val tweetyPieResult: Option[TweetyPie.TweetyPieResult] = candidate.tweetyPieResult
|
||||||
|
override lazy val tweetAuthor: Future[Option[User]] = Future.value(author)
|
||||||
|
override val target: PushTypes.Target = candidate.target
|
||||||
|
override val landingUrl: String = candidate.landingUrl
|
||||||
|
override val timeBoundedLandingUrl: Option[String] = candidate.timeBoundedLandingUrl
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
override val trendId: String = candidate.trendId
|
||||||
|
override val trendName: String = candidate.trendName
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
override val context: TrendsContext = candidate.context
|
||||||
|
}
|
||||||
|
|
||||||
|
case class TrendTweetPredicates(override val config: Config)
|
||||||
|
extends BasicTweetPredicatesForRFPH[TrendTweetPushCandidate] {
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
}
|
@ -0,0 +1,60 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.OutOfNetworkTweetCandidate
|
||||||
|
import com.twitter.frigate.common.base.TopicCandidate
|
||||||
|
import com.twitter.frigate.common.base.TripCandidate
|
||||||
|
import com.twitter.frigate.common.base.TweetAuthorDetails
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.OutOfNetworkTweetIbis2HydratorForCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.OutOfNetworkTweetNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.OutOfNetworkTweetPredicates
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie
|
||||||
|
import com.twitter.topiclisting.utt.LocalizedEntity
|
||||||
|
import com.twitter.trends.trip_v1.trip_tweets.thriftscala.TripDomain
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class TripTweetPushCandidate(
|
||||||
|
candidate: RawCandidate with OutOfNetworkTweetCandidate with TripCandidate,
|
||||||
|
author: Future[Option[User]],
|
||||||
|
copyIds: CopyIds
|
||||||
|
)(
|
||||||
|
implicit stats: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with TripCandidate
|
||||||
|
with TopicCandidate
|
||||||
|
with OutOfNetworkTweetCandidate
|
||||||
|
with TweetAuthorDetails
|
||||||
|
with OutOfNetworkTweetNTabRequestHydrator
|
||||||
|
with OutOfNetworkTweetIbis2HydratorForCandidate {
|
||||||
|
override val statsReceiver: StatsReceiver = stats
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
override val tweetId: Long = candidate.tweetId
|
||||||
|
override lazy val tweetyPieResult: Option[TweetyPie.TweetyPieResult] =
|
||||||
|
candidate.tweetyPieResult
|
||||||
|
override lazy val tweetAuthor: Future[Option[User]] = author
|
||||||
|
override val target: PushTypes.Target = candidate.target
|
||||||
|
override lazy val commonRecType: CommonRecommendationType =
|
||||||
|
candidate.commonRecType
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
override lazy val semanticCoreEntityId: Option[Long] = None
|
||||||
|
override lazy val localizedUttEntity: Option[LocalizedEntity] = None
|
||||||
|
override lazy val algorithmCR: Option[String] = None
|
||||||
|
override val tripDomain: Option[collection.Set[TripDomain]] = candidate.tripDomain
|
||||||
|
}
|
||||||
|
|
||||||
|
case class TripTweetCandidatePredicates(override val config: Config)
|
||||||
|
extends OutOfNetworkTweetPredicates[TripTweetPushCandidate] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,26 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.SocialContextActions
|
||||||
|
import com.twitter.frigate.common.base.TweetCandidate
|
||||||
|
import com.twitter.frigate.common.base.TweetDetails
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes._
|
||||||
|
import com.twitter.frigate.pushservice.config.Config
|
||||||
|
import com.twitter.frigate.pushservice.predicate._
|
||||||
|
import com.twitter.frigate.pushservice.take.predicates.BasicTweetPredicatesForRFPH
|
||||||
|
|
||||||
|
case class TweetActionCandidatePredicates(override val config: Config)
|
||||||
|
extends BasicTweetPredicatesForRFPH[
|
||||||
|
PushCandidate with TweetCandidate with TweetDetails with SocialContextActions
|
||||||
|
] {
|
||||||
|
|
||||||
|
implicit val statsReceiver: StatsReceiver = config.statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override val preCandidateSpecificPredicates = List(PredicatesForCandidate.minSocialContext(1))
|
||||||
|
|
||||||
|
override val postCandidateSpecificPredicates = List(
|
||||||
|
PredicatesForCandidate.socialContextBeingFollowed(config.edgeStore),
|
||||||
|
PredicatesForCandidate.socialContextBlockingOrMuting(config.edgeStore),
|
||||||
|
PredicatesForCandidate.socialContextNotRetweetFollowing(config.edgeStore)
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.SocialContextAction
|
||||||
|
import com.twitter.frigate.common.base.SocialContextUserDetails
|
||||||
|
import com.twitter.frigate.common.base.TweetAuthorDetails
|
||||||
|
import com.twitter.frigate.common.base.TweetFavoriteCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.TweetFavoriteCandidateIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.TweetFavoriteNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.util.CandidateHydrationUtil.TweetWithSocialContextTraits
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class TweetFavoritePushCandidate(
|
||||||
|
candidate: RawCandidate with TweetWithSocialContextTraits,
|
||||||
|
socialContextUserMap: Future[Map[Long, Option[User]]],
|
||||||
|
author: Future[Option[User]],
|
||||||
|
copyIds: CopyIds
|
||||||
|
)(
|
||||||
|
implicit stats: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with TweetFavoriteCandidate
|
||||||
|
with SocialContextUserDetails
|
||||||
|
with TweetAuthorDetails
|
||||||
|
with TweetFavoriteNTabRequestHydrator
|
||||||
|
with TweetFavoriteCandidateIbis2Hydrator {
|
||||||
|
override val statsReceiver: StatsReceiver = stats
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
override val tweetId: Long = candidate.tweetId
|
||||||
|
override val socialContextActions: Seq[SocialContextAction] =
|
||||||
|
candidate.socialContextActions
|
||||||
|
|
||||||
|
override val socialContextAllTypeActions: Seq[SocialContextAction] =
|
||||||
|
candidate.socialContextAllTypeActions
|
||||||
|
|
||||||
|
override lazy val scUserMap: Future[Map[Long, Option[User]]] = socialContextUserMap
|
||||||
|
override lazy val tweetAuthor: Future[Option[User]] = author
|
||||||
|
override lazy val commonRecType: CommonRecommendationType =
|
||||||
|
candidate.commonRecType
|
||||||
|
override val target: PushTypes.Target = candidate.target
|
||||||
|
override lazy val tweetyPieResult: Option[TweetyPie.TweetyPieResult] =
|
||||||
|
candidate.tweetyPieResult
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
}
|
@ -0,0 +1,51 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.SocialContextAction
|
||||||
|
import com.twitter.frigate.common.base.SocialContextUserDetails
|
||||||
|
import com.twitter.frigate.common.base.TweetAuthorDetails
|
||||||
|
import com.twitter.frigate.common.base.TweetRetweetCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.RawCandidate
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.model.candidate.CopyIds
|
||||||
|
import com.twitter.frigate.pushservice.model.ibis.TweetRetweetCandidateIbis2Hydrator
|
||||||
|
import com.twitter.frigate.pushservice.model.ntab.TweetRetweetNTabRequestHydrator
|
||||||
|
import com.twitter.frigate.pushservice.util.CandidateHydrationUtil.TweetWithSocialContextTraits
|
||||||
|
import com.twitter.frigate.thriftscala.CommonRecommendationType
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.stitch.tweetypie.TweetyPie
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
class TweetRetweetPushCandidate(
|
||||||
|
candidate: RawCandidate with TweetWithSocialContextTraits,
|
||||||
|
socialContextUserMap: Future[Map[Long, Option[User]]],
|
||||||
|
author: Future[Option[User]],
|
||||||
|
copyIds: CopyIds
|
||||||
|
)(
|
||||||
|
implicit stats: StatsReceiver,
|
||||||
|
pushModelScorer: PushMLModelScorer)
|
||||||
|
extends PushCandidate
|
||||||
|
with TweetRetweetCandidate
|
||||||
|
with SocialContextUserDetails
|
||||||
|
with TweetAuthorDetails
|
||||||
|
with TweetRetweetNTabRequestHydrator
|
||||||
|
with TweetRetweetCandidateIbis2Hydrator {
|
||||||
|
override val statsReceiver: StatsReceiver = stats
|
||||||
|
override val weightedOpenOrNtabClickModelScorer: PushMLModelScorer = pushModelScorer
|
||||||
|
override val tweetId: Long = candidate.tweetId
|
||||||
|
override val socialContextActions: Seq[SocialContextAction] =
|
||||||
|
candidate.socialContextActions
|
||||||
|
|
||||||
|
override val socialContextAllTypeActions: Seq[SocialContextAction] =
|
||||||
|
candidate.socialContextAllTypeActions
|
||||||
|
|
||||||
|
override lazy val scUserMap: Future[Map[Long, Option[User]]] = socialContextUserMap
|
||||||
|
override lazy val tweetAuthor: Future[Option[User]] = author
|
||||||
|
override lazy val commonRecType: CommonRecommendationType = candidate.commonRecType
|
||||||
|
override val target: PushTypes.Target = candidate.target
|
||||||
|
override lazy val tweetyPieResult: Option[TweetyPie.TweetyPieResult] = candidate.tweetyPieResult
|
||||||
|
override val pushCopyId: Option[Int] = copyIds.pushCopyId
|
||||||
|
override val ntabCopyId: Option[Int] = copyIds.ntabCopyId
|
||||||
|
override val copyAggregationId: Option[String] = copyIds.aggregationId
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model.candidate
|
||||||
|
|
||||||
|
import com.twitter.frigate.common.util.MRPushCopy
|
||||||
|
import com.twitter.frigate.common.util.MrPushCopyObjects
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.util.CandidateUtil
|
||||||
|
|
||||||
|
case class CopyIds(
|
||||||
|
pushCopyId: Option[Int] = None,
|
||||||
|
ntabCopyId: Option[Int] = None,
|
||||||
|
aggregationId: Option[String] = None)
|
||||||
|
|
||||||
|
trait CopyInfo {
|
||||||
|
self: PushCandidate =>
|
||||||
|
|
||||||
|
import com.twitter.frigate.data_pipeline.common.FrigateNotificationUtil._
|
||||||
|
|
||||||
|
def getPushCopy: Option[MRPushCopy] =
|
||||||
|
pushCopyId match {
|
||||||
|
case Some(pushCopyId) => MrPushCopyObjects.getCopyFromId(pushCopyId)
|
||||||
|
case _ =>
|
||||||
|
crt2PushCopy(
|
||||||
|
commonRecType,
|
||||||
|
CandidateUtil.getSocialContextActionsFromCandidate(self).size
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def pushCopyId: Option[Int]
|
||||||
|
|
||||||
|
def ntabCopyId: Option[Int]
|
||||||
|
|
||||||
|
def copyAggregationId: Option[String]
|
||||||
|
}
|
@ -0,0 +1,307 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model.candidate
|
||||||
|
|
||||||
|
import com.twitter.frigate.common.base.FeatureMap
|
||||||
|
import com.twitter.frigate.common.rec_types.RecTypes
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.ml.HydrationContextBuilder
|
||||||
|
import com.twitter.frigate.pushservice.ml.PushMLModelScorer
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.params.PushMLModel
|
||||||
|
import com.twitter.frigate.pushservice.params.WeightedOpenOrNtabClickModel
|
||||||
|
import com.twitter.nrel.hydration.push.HydrationContext
|
||||||
|
import com.twitter.timelines.configapi.FSParam
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import scala.collection.concurrent.{Map => CMap}
|
||||||
|
import scala.collection.convert.decorateAsScala._
|
||||||
|
|
||||||
|
trait MLScores {
|
||||||
|
|
||||||
|
self: PushCandidate =>
|
||||||
|
|
||||||
|
lazy val candidateHydrationContext: Future[HydrationContext] = HydrationContextBuilder.build(self)
|
||||||
|
|
||||||
|
def weightedOpenOrNtabClickModelScorer: PushMLModelScorer
|
||||||
|
|
||||||
|
// Used to store the scores and avoid duplicate prediction
|
||||||
|
private val qualityModelScores: CMap[
|
||||||
|
(PushMLModel.Value, WeightedOpenOrNtabClickModel.ModelNameType),
|
||||||
|
Future[Option[Double]]
|
||||||
|
] =
|
||||||
|
new ConcurrentHashMap[(PushMLModel.Value, WeightedOpenOrNtabClickModel.ModelNameType), Future[
|
||||||
|
Option[Double]
|
||||||
|
]]().asScala
|
||||||
|
|
||||||
|
def populateQualityModelScore(
|
||||||
|
pushMLModel: PushMLModel.Value,
|
||||||
|
modelVersion: WeightedOpenOrNtabClickModel.ModelNameType,
|
||||||
|
prob: Future[Option[Double]]
|
||||||
|
) = {
|
||||||
|
val modelAndVersion = (pushMLModel, modelVersion)
|
||||||
|
if (!qualityModelScores.contains(modelAndVersion)) {
|
||||||
|
qualityModelScores += modelAndVersion -> prob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ML scores that also depend on other candidates and are only available after all candidates are processed
|
||||||
|
// For example, the likelihood info for Importance Sampling
|
||||||
|
private lazy val crossCandidateMlScores: CMap[String, Double] =
|
||||||
|
new ConcurrentHashMap[String, Double]().asScala
|
||||||
|
|
||||||
|
def populateCrossCandidateMlScores(scoreName: String, score: Double): Unit = {
|
||||||
|
if (crossCandidateMlScores.contains(scoreName)) {
|
||||||
|
throw new Exception(
|
||||||
|
s"$scoreName has been populated in the CrossCandidateMlScores!\n" +
|
||||||
|
s"Existing crossCandidateMlScores are ${crossCandidateMlScores}\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
crossCandidateMlScores += scoreName -> score
|
||||||
|
}
|
||||||
|
|
||||||
|
def getMLModelScore(
|
||||||
|
pushMLModel: PushMLModel.Value,
|
||||||
|
modelVersion: WeightedOpenOrNtabClickModel.ModelNameType
|
||||||
|
): Future[Option[Double]] = {
|
||||||
|
qualityModelScores.getOrElseUpdate(
|
||||||
|
(pushMLModel, modelVersion),
|
||||||
|
weightedOpenOrNtabClickModelScorer
|
||||||
|
.singlePredicationForModelVersion(modelVersion, self, Some(pushMLModel))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getMLModelScoreWithoutUpdate(
|
||||||
|
pushMLModel: PushMLModel.Value,
|
||||||
|
modelVersion: WeightedOpenOrNtabClickModel.ModelNameType
|
||||||
|
): Future[Option[Double]] = {
|
||||||
|
qualityModelScores.getOrElse(
|
||||||
|
(pushMLModel, modelVersion),
|
||||||
|
Future.None
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getWeightedOpenOrNtabClickModelScore(
|
||||||
|
weightedOONCModelParam: FSParam[WeightedOpenOrNtabClickModel.ModelNameType]
|
||||||
|
): Future[Option[Double]] = {
|
||||||
|
getMLModelScore(
|
||||||
|
PushMLModel.WeightedOpenOrNtabClickProbability,
|
||||||
|
target.params(weightedOONCModelParam)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/* After we unify the ranking and filtering models, we follow the iteration process below
|
||||||
|
When improving the WeightedOONC model,
|
||||||
|
1) Run experiment which only replace the ranking model
|
||||||
|
2) Make decisions according to the experiment results
|
||||||
|
3) Use the ranking model for filtering
|
||||||
|
4) Adjust percentile thresholds if necessary
|
||||||
|
*/
|
||||||
|
lazy val mrWeightedOpenOrNtabClickRankingProbability: Future[Option[Double]] =
|
||||||
|
target.rankingModelParam.flatMap { modelParam =>
|
||||||
|
getWeightedOpenOrNtabClickModelScore(modelParam)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getBigFilteringScore(
|
||||||
|
pushMLModel: PushMLModel.Value,
|
||||||
|
modelVersion: WeightedOpenOrNtabClickModel.ModelNameType
|
||||||
|
): Future[Option[Double]] = {
|
||||||
|
mrWeightedOpenOrNtabClickRankingProbability.flatMap {
|
||||||
|
case Some(rankingScore) =>
|
||||||
|
// Adds ranking score to feature map (we must ensure the feature key is also in the feature context)
|
||||||
|
mergeFeatures(
|
||||||
|
FeatureMap(
|
||||||
|
numericFeatures = Map("scribe.WeightedOpenOrNtabClickProbability" -> rankingScore)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
getMLModelScore(pushMLModel, modelVersion)
|
||||||
|
case _ => Future.None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getWeightedOpenOrNtabClickScoreForScribing(): Seq[Future[Map[String, Double]]] = {
|
||||||
|
Seq(
|
||||||
|
mrWeightedOpenOrNtabClickRankingProbability.map {
|
||||||
|
case Some(score) => Map(PushMLModel.WeightedOpenOrNtabClickProbability.toString -> score)
|
||||||
|
case _ => Map.empty[String, Double]
|
||||||
|
},
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
target.rankingModelParam,
|
||||||
|
mrWeightedOpenOrNtabClickRankingProbability
|
||||||
|
).map {
|
||||||
|
case (rankingModelParam, Some(score)) =>
|
||||||
|
Map(target.params(rankingModelParam).toString -> score)
|
||||||
|
case _ => Map.empty[String, Double]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def getNsfwScoreForScribing(): Seq[Future[Map[String, Double]]] = {
|
||||||
|
val nsfwScoreFut = getMLModelScoreWithoutUpdate(
|
||||||
|
PushMLModel.HealthNsfwProbability,
|
||||||
|
target.params(PushFeatureSwitchParams.BqmlHealthModelTypeParam))
|
||||||
|
Seq(nsfwScoreFut.map { nsfwScoreOpt =>
|
||||||
|
nsfwScoreOpt
|
||||||
|
.map(nsfwScore => Map(PushMLModel.HealthNsfwProbability.toString -> nsfwScore)).getOrElse(
|
||||||
|
Map.empty[String, Double])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
def getBigFilteringSupervisedScoresForScribing(): Seq[Future[Map[String, Double]]] = {
|
||||||
|
if (target.params(
|
||||||
|
PushFeatureSwitchParams.EnableMrRequestScribingBigFilteringSupervisedScores)) {
|
||||||
|
Seq(
|
||||||
|
mrBigFilteringSupervisedSendingScore.map {
|
||||||
|
case Some(score) =>
|
||||||
|
Map(PushMLModel.BigFilteringSupervisedSendingModel.toString -> score)
|
||||||
|
case _ => Map.empty[String, Double]
|
||||||
|
},
|
||||||
|
mrBigFilteringSupervisedWithoutSendingScore.map {
|
||||||
|
case Some(score) =>
|
||||||
|
Map(PushMLModel.BigFilteringSupervisedWithoutSendingModel.toString -> score)
|
||||||
|
case _ => Map.empty[String, Double]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else Seq.empty[Future[Map[String, Double]]]
|
||||||
|
}
|
||||||
|
|
||||||
|
def getBigFilteringRLScoresForScribing(): Seq[Future[Map[String, Double]]] = {
|
||||||
|
if (target.params(PushFeatureSwitchParams.EnableMrRequestScribingBigFilteringRLScores)) {
|
||||||
|
Seq(
|
||||||
|
mrBigFilteringRLSendingScore.map {
|
||||||
|
case Some(score) => Map(PushMLModel.BigFilteringRLSendingModel.toString -> score)
|
||||||
|
case _ => Map.empty[String, Double]
|
||||||
|
},
|
||||||
|
mrBigFilteringRLWithoutSendingScore.map {
|
||||||
|
case Some(score) => Map(PushMLModel.BigFilteringRLWithoutSendingModel.toString -> score)
|
||||||
|
case _ => Map.empty[String, Double]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else Seq.empty[Future[Map[String, Double]]]
|
||||||
|
}
|
||||||
|
|
||||||
|
def buildModelScoresSeqForScribing(): Seq[Future[Map[String, Double]]] = {
|
||||||
|
getWeightedOpenOrNtabClickScoreForScribing() ++
|
||||||
|
getBigFilteringSupervisedScoresForScribing() ++
|
||||||
|
getBigFilteringRLScoresForScribing() ++
|
||||||
|
getNsfwScoreForScribing()
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val mrBigFilteringSupervisedSendingScore: Future[Option[Double]] =
|
||||||
|
getBigFilteringScore(
|
||||||
|
PushMLModel.BigFilteringSupervisedSendingModel,
|
||||||
|
target.params(PushFeatureSwitchParams.BigFilteringSupervisedSendingModelParam)
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val mrBigFilteringSupervisedWithoutSendingScore: Future[Option[Double]] =
|
||||||
|
getBigFilteringScore(
|
||||||
|
PushMLModel.BigFilteringSupervisedWithoutSendingModel,
|
||||||
|
target.params(PushFeatureSwitchParams.BigFilteringSupervisedWithoutSendingModelParam)
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val mrBigFilteringRLSendingScore: Future[Option[Double]] =
|
||||||
|
getBigFilteringScore(
|
||||||
|
PushMLModel.BigFilteringRLSendingModel,
|
||||||
|
target.params(PushFeatureSwitchParams.BigFilteringRLSendingModelParam)
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val mrBigFilteringRLWithoutSendingScore: Future[Option[Double]] =
|
||||||
|
getBigFilteringScore(
|
||||||
|
PushMLModel.BigFilteringRLWithoutSendingModel,
|
||||||
|
target.params(PushFeatureSwitchParams.BigFilteringRLWithoutSendingModelParam)
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val mrWeightedOpenOrNtabClickFilteringProbability: Future[Option[Double]] =
|
||||||
|
getWeightedOpenOrNtabClickModelScore(
|
||||||
|
target.filteringModelParam
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val mrQualityUprankingProbability: Future[Option[Double]] =
|
||||||
|
getMLModelScore(
|
||||||
|
PushMLModel.FilteringProbability,
|
||||||
|
target.params(PushFeatureSwitchParams.QualityUprankingModelTypeParam)
|
||||||
|
)
|
||||||
|
|
||||||
|
lazy val mrNsfwScore: Future[Option[Double]] =
|
||||||
|
getMLModelScoreWithoutUpdate(
|
||||||
|
PushMLModel.HealthNsfwProbability,
|
||||||
|
target.params(PushFeatureSwitchParams.BqmlHealthModelTypeParam)
|
||||||
|
)
|
||||||
|
|
||||||
|
// MR quality upranking param
|
||||||
|
private val qualityUprankingBoost: String = "QualityUprankingBoost"
|
||||||
|
private val producerQualityUprankingBoost: String = "ProducerQualityUprankingBoost"
|
||||||
|
private val qualityUprankingInfo: CMap[String, Double] =
|
||||||
|
new ConcurrentHashMap[String, Double]().asScala
|
||||||
|
|
||||||
|
lazy val mrQualityUprankingBoost: Option[Double] =
|
||||||
|
qualityUprankingInfo.get(qualityUprankingBoost)
|
||||||
|
lazy val mrProducerQualityUprankingBoost: Option[Double] =
|
||||||
|
qualityUprankingInfo.get(producerQualityUprankingBoost)
|
||||||
|
|
||||||
|
def setQualityUprankingBoost(boost: Double) =
|
||||||
|
if (qualityUprankingInfo.contains(qualityUprankingBoost)) {
|
||||||
|
qualityUprankingInfo(qualityUprankingBoost) = boost
|
||||||
|
} else {
|
||||||
|
qualityUprankingInfo += qualityUprankingBoost -> boost
|
||||||
|
}
|
||||||
|
def setProducerQualityUprankingBoost(boost: Double) =
|
||||||
|
if (qualityUprankingInfo.contains(producerQualityUprankingBoost)) {
|
||||||
|
qualityUprankingInfo(producerQualityUprankingBoost) = boost
|
||||||
|
} else {
|
||||||
|
qualityUprankingInfo += producerQualityUprankingBoost -> boost
|
||||||
|
}
|
||||||
|
|
||||||
|
private lazy val mrModelScoresFut: Future[Map[String, Double]] = {
|
||||||
|
if (self.target.isLoggedOutUser) {
|
||||||
|
Future.value(Map.empty[String, Double])
|
||||||
|
} else {
|
||||||
|
Future
|
||||||
|
.collectToTry {
|
||||||
|
buildModelScoresSeqForScribing()
|
||||||
|
}.map { scoreTrySeq =>
|
||||||
|
scoreTrySeq
|
||||||
|
.collect {
|
||||||
|
case result if result.isReturn => result.get()
|
||||||
|
}.reduce(_ ++ _)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal model scores (scores that are independent of other candidates) for scribing
|
||||||
|
lazy val modelScores: Future[Map[String, Double]] =
|
||||||
|
target.dauProbability.flatMap { dauProbabilityOpt =>
|
||||||
|
val dauProbScoreMap = dauProbabilityOpt
|
||||||
|
.map(_.probability).map { dauProb =>
|
||||||
|
PushMLModel.DauProbability.toString -> dauProb
|
||||||
|
}.toMap
|
||||||
|
|
||||||
|
// Avoid unnecessary MR model scribing
|
||||||
|
if (target.isDarkWrite) {
|
||||||
|
mrModelScoresFut.map(dauProbScoreMap ++ _)
|
||||||
|
} else if (RecTypes.isSendHandlerType(commonRecType) && !RecTypes
|
||||||
|
.sendHandlerTypesUsingMrModel(commonRecType)) {
|
||||||
|
Future.value(dauProbScoreMap)
|
||||||
|
} else {
|
||||||
|
mrModelScoresFut.map(dauProbScoreMap ++ _)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will scribe both internal ML scores and cross-Candidate scores
|
||||||
|
def getModelScoresforScribing(): Future[Map[String, Double]] = {
|
||||||
|
if (RecTypes.notEligibleForModelScoreTracking(commonRecType) || self.target.isLoggedOutUser) {
|
||||||
|
Future.value(Map.empty[String, Double])
|
||||||
|
} else {
|
||||||
|
modelScores.map { internalScores =>
|
||||||
|
if (internalScores.keySet.intersect(crossCandidateMlScores.keySet).nonEmpty) {
|
||||||
|
throw new Exception(
|
||||||
|
"crossCandidateMlScores overlap internalModelScores\n" +
|
||||||
|
s"internalScores keySet: ${internalScores.keySet}\n" +
|
||||||
|
s"crossCandidateScores keySet: ${crossCandidateMlScores.keySet}\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internalScores ++ crossCandidateMlScores
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,104 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model.candidate
|
||||||
|
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.params.HighQualityScribingScores
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.params.PushMLModel
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import scala.collection.concurrent.{Map => CMap}
|
||||||
|
import scala.collection.convert.decorateAsScala._
|
||||||
|
|
||||||
|
trait QualityScribing {
|
||||||
|
self: PushCandidate with MLScores =>
|
||||||
|
|
||||||
|
// Use to store other scores (to avoid duplicate queries to other services, e.g. HSS)
|
||||||
|
private val externalCachedScores: CMap[String, Future[Option[Double]]] =
|
||||||
|
new ConcurrentHashMap[String, Future[Option[Double]]]().asScala
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the model version as specified by the corresponding FS param.
|
||||||
|
* This model version will be used for getting the cached score or triggering
|
||||||
|
* a prediction request.
|
||||||
|
*
|
||||||
|
* @param modelName The score we will like to scribe
|
||||||
|
*/
|
||||||
|
private def getModelVersion(
|
||||||
|
modelName: HighQualityScribingScores.Name
|
||||||
|
): String = {
|
||||||
|
modelName match {
|
||||||
|
case HighQualityScribingScores.HeavyRankingScore =>
|
||||||
|
target.params(PushFeatureSwitchParams.HighQualityCandidatesHeavyRankingModel)
|
||||||
|
case HighQualityScribingScores.NonPersonalizedQualityScoreUsingCnn =>
|
||||||
|
target.params(PushFeatureSwitchParams.HighQualityCandidatesNonPersonalizedQualityCnnModel)
|
||||||
|
case HighQualityScribingScores.BqmlNsfwScore =>
|
||||||
|
target.params(PushFeatureSwitchParams.HighQualityCandidatesBqmlNsfwModel)
|
||||||
|
case HighQualityScribingScores.BqmlReportScore =>
|
||||||
|
target.params(PushFeatureSwitchParams.HighQualityCandidatesBqmlReportModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the score for scribing either from a cached value or
|
||||||
|
* by generating a prediction request. This will increase model QPS
|
||||||
|
*
|
||||||
|
* @param pushMLModel This represents the prefix of the model name (i.e. [pushMLModel]_[version])
|
||||||
|
* @param scoreName The name to be use when scribing this score
|
||||||
|
*/
|
||||||
|
def getScribingScore(
|
||||||
|
pushMLModel: PushMLModel.Value,
|
||||||
|
scoreName: HighQualityScribingScores.Name
|
||||||
|
): Future[(String, Option[Double])] = {
|
||||||
|
getMLModelScore(
|
||||||
|
pushMLModel,
|
||||||
|
getModelVersion(scoreName)
|
||||||
|
).map { scoreOpt =>
|
||||||
|
scoreName.toString -> scoreOpt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the score for scribing if it has been computed/cached before otherwise
|
||||||
|
* it will return Future.None
|
||||||
|
*
|
||||||
|
* @param pushMLModel This represents the prefix of the model name (i.e. [pushMLModel]_[version])
|
||||||
|
* @param scoreName The name to be use when scribing this score
|
||||||
|
*/
|
||||||
|
def getScribingScoreWithoutUpdate(
|
||||||
|
pushMLModel: PushMLModel.Value,
|
||||||
|
scoreName: HighQualityScribingScores.Name
|
||||||
|
): Future[(String, Option[Double])] = {
|
||||||
|
getMLModelScoreWithoutUpdate(
|
||||||
|
pushMLModel,
|
||||||
|
getModelVersion(scoreName)
|
||||||
|
).map { scoreOpt =>
|
||||||
|
scoreName.toString -> scoreOpt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Caches the given score future
|
||||||
|
*
|
||||||
|
* @param scoreName The name to be use when scribing this score
|
||||||
|
* @param scoreFut Future mapping scoreName -> scoreOpt
|
||||||
|
*/
|
||||||
|
def cacheExternalScore(scoreName: String, scoreFut: Future[Option[Double]]) = {
|
||||||
|
if (!externalCachedScores.contains(scoreName)) {
|
||||||
|
externalCachedScores += scoreName -> scoreFut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns all external scores future cached as a sequence
|
||||||
|
*/
|
||||||
|
def getExternalCachedScores: Seq[Future[(String, Option[Double])]] = {
|
||||||
|
externalCachedScores.map {
|
||||||
|
case (modelName, scoreFut) =>
|
||||||
|
scoreFut.map { scoreOpt => modelName -> scoreOpt }
|
||||||
|
}.toSeq
|
||||||
|
}
|
||||||
|
|
||||||
|
def getExternalCachedScoreByName(name: String): Future[Option[Double]] = {
|
||||||
|
externalCachedScores.getOrElse(name, Future.None)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,277 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model.candidate
|
||||||
|
|
||||||
|
import com.twitter.frigate.data_pipeline.features_common.PushQualityModelFeatureContext.featureContext
|
||||||
|
import com.twitter.frigate.data_pipeline.features_common.PushQualityModelUtil
|
||||||
|
import com.twitter.frigate.pushservice.params.PushFeatureSwitchParams
|
||||||
|
import com.twitter.frigate.pushservice.params.PushParams
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base._
|
||||||
|
import com.twitter.frigate.common.rec_types.RecTypes
|
||||||
|
import com.twitter.frigate.common.util.NotificationScribeUtil
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.OutOfNetworkTweetPushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.TopicProofTweetPushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.ml.HydrationContextBuilder
|
||||||
|
import com.twitter.frigate.pushservice.predicate.quality_model_predicate.PDauCohort
|
||||||
|
import com.twitter.frigate.pushservice.predicate.quality_model_predicate.PDauCohortUtil
|
||||||
|
import com.twitter.frigate.pushservice.util.Candidate2FrigateNotification
|
||||||
|
import com.twitter.frigate.pushservice.util.MediaAnnotationsUtil.sensitiveMediaCategoryFeatureName
|
||||||
|
import com.twitter.frigate.scribe.thriftscala.FrigateNotificationScribeType
|
||||||
|
import com.twitter.frigate.scribe.thriftscala.NotificationScribe
|
||||||
|
import com.twitter.frigate.scribe.thriftscala.PredicateDetailedInfo
|
||||||
|
import com.twitter.frigate.scribe.thriftscala.PushCapInfo
|
||||||
|
import com.twitter.frigate.thriftscala.ChannelName
|
||||||
|
import com.twitter.frigate.thriftscala.FrigateNotification
|
||||||
|
import com.twitter.frigate.thriftscala.OverrideInfo
|
||||||
|
import com.twitter.gizmoduck.thriftscala.User
|
||||||
|
import com.twitter.hermit.model.user_state.UserState.UserState
|
||||||
|
import com.twitter.ibis2.service.thriftscala.Ibis2Response
|
||||||
|
import com.twitter.ml.api.util.ScalaToJavaDataRecordConversions
|
||||||
|
import com.twitter.nrel.heavyranker.FeatureHydrator
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import java.util.UUID
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import scala.collection.concurrent.{Map => CMap}
|
||||||
|
import scala.collection.Map
|
||||||
|
import scala.collection.convert.decorateAsScala._
|
||||||
|
|
||||||
|
trait Scriber {
|
||||||
|
self: PushCandidate =>
|
||||||
|
|
||||||
|
def statsReceiver: StatsReceiver
|
||||||
|
|
||||||
|
def frigateNotification: FrigateNotification = Candidate2FrigateNotification
|
||||||
|
.getFrigateNotification(self)(statsReceiver)
|
||||||
|
.copy(copyAggregationId = self.copyAggregationId)
|
||||||
|
|
||||||
|
lazy val impressionId: String = UUID.randomUUID.toString.replaceAll("-", "")
|
||||||
|
|
||||||
|
// Used to store the score and threshold for predicates
|
||||||
|
// Map(predicate name, (score, threshold, filter?))
|
||||||
|
private val predicateScoreAndThreshold: CMap[String, PredicateDetailedInfo] =
|
||||||
|
new ConcurrentHashMap[String, PredicateDetailedInfo]().asScala
|
||||||
|
|
||||||
|
def cachePredicateInfo(
|
||||||
|
predName: String,
|
||||||
|
predScore: Double,
|
||||||
|
predThreshold: Double,
|
||||||
|
predResult: Boolean,
|
||||||
|
additionalInformation: Option[Map[String, Double]] = None
|
||||||
|
) = {
|
||||||
|
if (!predicateScoreAndThreshold.contains(predName)) {
|
||||||
|
predicateScoreAndThreshold += predName -> PredicateDetailedInfo(
|
||||||
|
predName,
|
||||||
|
predScore,
|
||||||
|
predThreshold,
|
||||||
|
predResult,
|
||||||
|
additionalInformation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getCachedPredicateInfo(): Seq[PredicateDetailedInfo] = predicateScoreAndThreshold.values.toSeq
|
||||||
|
|
||||||
|
def frigateNotificationForPersistence(
|
||||||
|
channels: Seq[ChannelName],
|
||||||
|
isSilentPush: Boolean,
|
||||||
|
overrideInfoOpt: Option[OverrideInfo] = None,
|
||||||
|
copyFeaturesList: Set[String]
|
||||||
|
): Future[FrigateNotification] = {
|
||||||
|
|
||||||
|
// record display location for frigate notification
|
||||||
|
statsReceiver
|
||||||
|
.scope("FrigateNotificationForPersistence")
|
||||||
|
.scope("displayLocation")
|
||||||
|
.counter(frigateNotification.notificationDisplayLocation.name)
|
||||||
|
.incr()
|
||||||
|
|
||||||
|
val getModelScores = self.getModelScoresforScribing()
|
||||||
|
|
||||||
|
Future.join(getModelScores, self.target.targetMrUserState).map {
|
||||||
|
case (mlScores, mrUserState) =>
|
||||||
|
frigateNotification.copy(
|
||||||
|
impressionId = Some(impressionId),
|
||||||
|
isSilentPush = Some(isSilentPush),
|
||||||
|
overrideInfo = overrideInfoOpt,
|
||||||
|
mlModelScores = Some(mlScores),
|
||||||
|
mrUserState = mrUserState.map(_.name),
|
||||||
|
copyFeatures = Some(copyFeaturesList.toSeq)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// scribe data
|
||||||
|
private def getNotificationScribe(
|
||||||
|
notifForPersistence: FrigateNotification,
|
||||||
|
userState: Option[UserState],
|
||||||
|
dauCohort: PDauCohort.Value,
|
||||||
|
ibis2Response: Option[Ibis2Response],
|
||||||
|
tweetAuthorId: Option[Long],
|
||||||
|
recUserId: Option[Long],
|
||||||
|
modelScoresMap: Option[Map[String, Double]],
|
||||||
|
primaryClient: Option[String],
|
||||||
|
isMrBackfillCR: Option[Boolean] = None,
|
||||||
|
tagsCR: Option[Seq[String]] = None,
|
||||||
|
gizmoduckTargetUser: Option[User],
|
||||||
|
predicateDetailedInfoList: Option[Seq[PredicateDetailedInfo]] = None,
|
||||||
|
pushCapInfoList: Option[Seq[PushCapInfo]] = None
|
||||||
|
): NotificationScribe = {
|
||||||
|
NotificationScribe(
|
||||||
|
FrigateNotificationScribeType.SendMessage,
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
targetUserId = Some(self.target.targetId),
|
||||||
|
timestampKeyForHistoryV2 = Some(createdAt.inSeconds),
|
||||||
|
sendType = NotificationScribeUtil.convertToScribeDisplayLocation(
|
||||||
|
self.frigateNotification.notificationDisplayLocation
|
||||||
|
),
|
||||||
|
recommendationType = NotificationScribeUtil.convertToScribeRecommendationType(
|
||||||
|
self.frigateNotification.commonRecommendationType
|
||||||
|
),
|
||||||
|
commonRecommendationType = Some(self.frigateNotification.commonRecommendationType),
|
||||||
|
fromPushService = Some(true),
|
||||||
|
frigateNotification = Some(notifForPersistence),
|
||||||
|
impressionId = Some(impressionId),
|
||||||
|
skipModelInfo = target.skipModelInfo,
|
||||||
|
ibis2Response = ibis2Response,
|
||||||
|
tweetAuthorId = tweetAuthorId,
|
||||||
|
scribeFeatures = Some(target.noSkipButScribeFeatures),
|
||||||
|
userState = userState.map(_.toString),
|
||||||
|
pDauCohort = Some(dauCohort.toString),
|
||||||
|
recommendedUserId = recUserId,
|
||||||
|
modelScores = modelScoresMap,
|
||||||
|
primaryClient = primaryClient,
|
||||||
|
isMrBackfillCR = isMrBackfillCR,
|
||||||
|
tagsCR = tagsCR,
|
||||||
|
targetUserType = gizmoduckTargetUser.map(_.userType),
|
||||||
|
predicateDetailedInfoList = predicateDetailedInfoList,
|
||||||
|
pushCapInfoList = pushCapInfoList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
def scribeData(
|
||||||
|
ibis2Response: Option[Ibis2Response] = None,
|
||||||
|
isSilentPush: Boolean = false,
|
||||||
|
overrideInfoOpt: Option[OverrideInfo] = None,
|
||||||
|
copyFeaturesList: Set[String] = Set.empty,
|
||||||
|
channels: Seq[ChannelName] = Seq.empty
|
||||||
|
): Future[NotificationScribe] = {
|
||||||
|
|
||||||
|
val recTweetAuthorId = self match {
|
||||||
|
case t: TweetCandidate with TweetAuthor => t.authorId
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
val recUserId = self match {
|
||||||
|
case u: UserCandidate => Some(u.userId)
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
val isMrBackfillCR = self match {
|
||||||
|
case t: OutOfNetworkTweetPushCandidate => t.isMrBackfillCR
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
val tagsCR = self match {
|
||||||
|
case t: OutOfNetworkTweetPushCandidate =>
|
||||||
|
t.tagsCR.map { tags =>
|
||||||
|
tags.map(_.toString)
|
||||||
|
}
|
||||||
|
case t: TopicProofTweetPushCandidate =>
|
||||||
|
t.tagsCR.map { tags =>
|
||||||
|
tags.map(_.toString)
|
||||||
|
}
|
||||||
|
case _ => None
|
||||||
|
}
|
||||||
|
|
||||||
|
Future
|
||||||
|
.join(
|
||||||
|
frigateNotificationForPersistence(
|
||||||
|
channels = channels,
|
||||||
|
isSilentPush = isSilentPush,
|
||||||
|
overrideInfoOpt = overrideInfoOpt,
|
||||||
|
copyFeaturesList = copyFeaturesList
|
||||||
|
),
|
||||||
|
target.targetUserState,
|
||||||
|
PDauCohortUtil.getPDauCohort(target),
|
||||||
|
target.deviceInfo,
|
||||||
|
target.targetUser
|
||||||
|
)
|
||||||
|
.flatMap {
|
||||||
|
case (notifForPersistence, userState, dauCohort, deviceInfo, gizmoduckTargetUserOpt) =>
|
||||||
|
val primaryClient = deviceInfo.flatMap(_.guessedPrimaryClient).map(_.toString)
|
||||||
|
val cachedPredicateInfo =
|
||||||
|
if (self.target.params(PushParams.EnablePredicateDetailedInfoScribing)) {
|
||||||
|
Some(getCachedPredicateInfo())
|
||||||
|
} else None
|
||||||
|
|
||||||
|
val cachedPushCapInfo =
|
||||||
|
if (self.target
|
||||||
|
.params(PushParams.EnablePushCapInfoScribing)) {
|
||||||
|
Some(target.finalPushcapAndFatigue.values.toSeq)
|
||||||
|
} else None
|
||||||
|
|
||||||
|
val data = getNotificationScribe(
|
||||||
|
notifForPersistence,
|
||||||
|
userState,
|
||||||
|
dauCohort,
|
||||||
|
ibis2Response,
|
||||||
|
recTweetAuthorId,
|
||||||
|
recUserId,
|
||||||
|
notifForPersistence.mlModelScores,
|
||||||
|
primaryClient,
|
||||||
|
isMrBackfillCR,
|
||||||
|
tagsCR,
|
||||||
|
gizmoduckTargetUserOpt,
|
||||||
|
cachedPredicateInfo,
|
||||||
|
cachedPushCapInfo
|
||||||
|
)
|
||||||
|
//Don't scribe features for CRTs not eligible for ML Layer
|
||||||
|
if ((target.isModelTrainingData || target.scribeFeatureWithoutHydratingNewFeatures)
|
||||||
|
&& !RecTypes.notEligibleForModelScoreTracking(self.commonRecType)) {
|
||||||
|
// scribe all the features for the model training data
|
||||||
|
self.getFeaturesForScribing.map { scribedFeatureMap =>
|
||||||
|
if (target.params(PushParams.EnableScribingMLFeaturesAsDataRecord) && !target.params(
|
||||||
|
PushFeatureSwitchParams.EnableMrScribingMLFeaturesAsFeatureMapForStaging)) {
|
||||||
|
val scribedFeatureDataRecord =
|
||||||
|
ScalaToJavaDataRecordConversions.javaDataRecord2ScalaDataRecord(
|
||||||
|
PushQualityModelUtil.adaptToDataRecord(scribedFeatureMap, featureContext))
|
||||||
|
data.copy(
|
||||||
|
featureDataRecord = Some(scribedFeatureDataRecord)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
data.copy(features =
|
||||||
|
Some(PushQualityModelUtil.convertFeatureMapToFeatures(scribedFeatureMap)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else Future.value(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def getFeaturesForScribing: Future[FeatureMap] = {
|
||||||
|
target.featureMap
|
||||||
|
.flatMap { targetFeatureMap =>
|
||||||
|
val onlineFeatureMap = targetFeatureMap ++ self
|
||||||
|
.candidateFeatureMap() // targetFeatureMap includes target core user history features
|
||||||
|
|
||||||
|
val filteredFeatureMap = {
|
||||||
|
onlineFeatureMap.copy(
|
||||||
|
sparseContinuousFeatures = onlineFeatureMap.sparseContinuousFeatures.filterKeys(
|
||||||
|
!_.equals(sensitiveMediaCategoryFeatureName))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val targetHydrationContext = HydrationContextBuilder.build(self.target)
|
||||||
|
val candidateHydrationContext = HydrationContextBuilder.build(self)
|
||||||
|
|
||||||
|
val featureMapFut = targetHydrationContext.join(candidateHydrationContext).flatMap {
|
||||||
|
case (targetContext, candidateContext) =>
|
||||||
|
FeatureHydrator.getFeatures(
|
||||||
|
candidateHydrationContext = candidateContext,
|
||||||
|
targetHydrationContext = targetContext,
|
||||||
|
onlineFeatures = filteredFeatureMap,
|
||||||
|
statsReceiver = statsReceiver)
|
||||||
|
}
|
||||||
|
|
||||||
|
featureMapFut
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,25 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model.ibis
|
||||||
|
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.ibis2.lib.util.JsonMarshal
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
trait CustomConfigurationMapForIbis {
|
||||||
|
self: PushCandidate =>
|
||||||
|
|
||||||
|
lazy val customConfigMapsJsonFut: Future[String] = {
|
||||||
|
customFieldsMapFut.map { customFields =>
|
||||||
|
JsonMarshal.toJson(customFields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val customConfigMapsFut: Future[Map[String, String]] = {
|
||||||
|
if (self.target.isLoggedOutUser) {
|
||||||
|
Future.value(Map.empty[String, String])
|
||||||
|
} else {
|
||||||
|
customConfigMapsJsonFut.map { customConfigMapsJson =>
|
||||||
|
Map("custom_config" -> customConfigMapsJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,17 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model.ibis
|
||||||
|
|
||||||
|
import com.twitter.frigate.common.base.DiscoverTwitterCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.util.PushIbisUtil.mergeFutModelValues
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
trait DiscoverTwitterPushIbis2Hydrator extends Ibis2HydratorForCandidate {
|
||||||
|
self: PushCandidate with DiscoverTwitterCandidate =>
|
||||||
|
|
||||||
|
private lazy val targetModelValues: Map[String, String] = Map(
|
||||||
|
"target_user" -> target.targetId.toString
|
||||||
|
)
|
||||||
|
|
||||||
|
override lazy val modelValues: Future[Map[String, String]] =
|
||||||
|
mergeFutModelValues(super.modelValues, Future.value(targetModelValues))
|
||||||
|
}
|
@ -0,0 +1,24 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model.ibis
|
||||||
|
|
||||||
|
import com.twitter.finagle.stats.StatsReceiver
|
||||||
|
import com.twitter.frigate.common.base.F1FirstDegree
|
||||||
|
import com.twitter.frigate.common.base.TweetAuthorDetails
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
trait F1FirstDegreeTweetIbis2HydratorForCandidate
|
||||||
|
extends TweetCandidateIbis2Hydrator
|
||||||
|
with RankedSocialContextIbis2Hydrator {
|
||||||
|
self: PushCandidate with F1FirstDegree with TweetAuthorDetails =>
|
||||||
|
|
||||||
|
override lazy val scopedStats: StatsReceiver = statsReceiver.scope(getClass.getSimpleName)
|
||||||
|
|
||||||
|
override lazy val tweetModelValues: Future[Map[String, String]] = {
|
||||||
|
for {
|
||||||
|
superModelValues <- super.tweetModelValues
|
||||||
|
tweetInlineModelValues <- tweetInlineActionModelValue
|
||||||
|
} yield {
|
||||||
|
superModelValues ++ otherModelValues ++ mediaModelValue ++ tweetInlineModelValues ++ inlineVideoMediaMap
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,127 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model.ibis
|
||||||
|
|
||||||
|
import com.twitter.frigate.common.rec_types.RecTypes
|
||||||
|
import com.twitter.frigate.common.util.MRPushCopy
|
||||||
|
import com.twitter.frigate.common.util.MrPushCopyObjects
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.params.{PushFeatureSwitchParams => FS}
|
||||||
|
import com.twitter.ibis2.service.thriftscala.Flags
|
||||||
|
import com.twitter.ibis2.service.thriftscala.Ibis2Request
|
||||||
|
import com.twitter.ibis2.service.thriftscala.RecipientSelector
|
||||||
|
import com.twitter.ibis2.service.thriftscala.ResponseFlags
|
||||||
|
import com.twitter.util.Future
|
||||||
|
import scala.util.control.NoStackTrace
|
||||||
|
import com.twitter.ni.lib.logged_out_transform.Ibis2RequestTransform
|
||||||
|
|
||||||
|
class PushCopyIdNotFoundException(private val message: String)
|
||||||
|
extends Exception(message)
|
||||||
|
with NoStackTrace
|
||||||
|
|
||||||
|
class InvalidPushCopyIdException(private val message: String)
|
||||||
|
extends Exception(message)
|
||||||
|
with NoStackTrace
|
||||||
|
|
||||||
|
trait Ibis2HydratorForCandidate
|
||||||
|
extends CandidatePushCopy
|
||||||
|
with OverrideForIbis2Request
|
||||||
|
with CustomConfigurationMapForIbis {
|
||||||
|
self: PushCandidate =>
|
||||||
|
|
||||||
|
lazy val silentPushModelValue: Map[String, String] =
|
||||||
|
if (RecTypes.silentPushDefaultEnabledCrts.contains(commonRecType)) {
|
||||||
|
Map.empty
|
||||||
|
} else {
|
||||||
|
Map("is_silent_push" -> "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
private def transformRelevanceScore(
|
||||||
|
mlScore: Double,
|
||||||
|
scoreRange: Seq[Double]
|
||||||
|
): Double = {
|
||||||
|
val (lowerBound, upperBound) = (scoreRange.head, scoreRange.last)
|
||||||
|
(mlScore * (upperBound - lowerBound)) + lowerBound
|
||||||
|
}
|
||||||
|
|
||||||
|
private def getBoundedMlScore(mlScore: Double): Double = {
|
||||||
|
if (RecTypes.isMagicFanoutEventType(commonRecType)) {
|
||||||
|
val mfScoreRange = target.params(FS.MagicFanoutRelevanceScoreRange)
|
||||||
|
transformRelevanceScore(mlScore, mfScoreRange)
|
||||||
|
} else {
|
||||||
|
val mrScoreRange = target.params(FS.MagicRecsRelevanceScoreRange)
|
||||||
|
transformRelevanceScore(mlScore, mrScoreRange)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lazy val relevanceScoreMapFut: Future[Map[String, String]] = {
|
||||||
|
mrWeightedOpenOrNtabClickRankingProbability.map {
|
||||||
|
case Some(mlScore) if target.params(FS.IncludeRelevanceScoreInIbis2Payload) =>
|
||||||
|
val boundedMlScore = getBoundedMlScore(mlScore)
|
||||||
|
Map("relevance_score" -> boundedMlScore.toString)
|
||||||
|
case _ => Map.empty[String, String]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def customFieldsMapFut: Future[Map[String, String]] = relevanceScoreMapFut
|
||||||
|
|
||||||
|
//override is only enabled for RFPH CRT
|
||||||
|
def modelValues: Future[Map[String, String]] = {
|
||||||
|
Future.join(overrideModelValueFut, customConfigMapsFut).map {
|
||||||
|
case (overrideModelValue, customConfig) =>
|
||||||
|
overrideModelValue ++ silentPushModelValue ++ customConfig
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def modelName: String = pushCopy.ibisPushModelName
|
||||||
|
|
||||||
|
def senderId: Option[Long] = None
|
||||||
|
|
||||||
|
def ibis2Request: Future[Option[Ibis2Request]] = {
|
||||||
|
Future.join(self.target.loggedOutMetadata, modelValues).map {
|
||||||
|
case (Some(metadata), modelVals) =>
|
||||||
|
Some(
|
||||||
|
Ibis2RequestTransform
|
||||||
|
.apply(metadata, modelName, modelVals).copy(
|
||||||
|
senderId = senderId,
|
||||||
|
flags = Some(Flags(
|
||||||
|
darkWrite = Some(target.isDarkWrite),
|
||||||
|
skipDupcheck = target.pushContext.flatMap(_.useDebugHandler),
|
||||||
|
responseFlags = Some(ResponseFlags(stringTelemetry = Some(true)))
|
||||||
|
))
|
||||||
|
))
|
||||||
|
case (None, modelVals) =>
|
||||||
|
Some(
|
||||||
|
Ibis2Request(
|
||||||
|
recipientSelector = RecipientSelector(Some(target.targetId)),
|
||||||
|
modelName = modelName,
|
||||||
|
modelValues = Some(modelVals),
|
||||||
|
senderId = senderId,
|
||||||
|
flags = Some(
|
||||||
|
Flags(
|
||||||
|
darkWrite = Some(target.isDarkWrite),
|
||||||
|
skipDupcheck = target.pushContext.flatMap(_.useDebugHandler),
|
||||||
|
responseFlags = Some(ResponseFlags(stringTelemetry = Some(true)))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
trait CandidatePushCopy {
|
||||||
|
self: PushCandidate =>
|
||||||
|
|
||||||
|
final lazy val pushCopy: MRPushCopy =
|
||||||
|
pushCopyId match {
|
||||||
|
case Some(pushCopyId) =>
|
||||||
|
MrPushCopyObjects
|
||||||
|
.getCopyFromId(pushCopyId)
|
||||||
|
.getOrElse(
|
||||||
|
throw new InvalidPushCopyIdException(
|
||||||
|
s"Invalid push copy id: $pushCopyId for ${self.commonRecType}"))
|
||||||
|
|
||||||
|
case None =>
|
||||||
|
throw new PushCopyIdNotFoundException(
|
||||||
|
s"PushCopy not found in frigateNotification for ${self.commonRecType}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,12 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model.ibis
|
||||||
|
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.util.InlineActionUtil
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
trait InlineActionIbis2Hydrator {
|
||||||
|
self: PushCandidate =>
|
||||||
|
|
||||||
|
lazy val tweetInlineActionModelValue: Future[Map[String, String]] =
|
||||||
|
InlineActionUtil.getTweetInlineActionValue(target)
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model.ibis
|
||||||
|
|
||||||
|
import com.twitter.frigate.pushservice.model.ListRecommendationPushCandidate
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
trait ListIbis2Hydrator extends Ibis2HydratorForCandidate {
|
||||||
|
self: ListRecommendationPushCandidate =>
|
||||||
|
|
||||||
|
override lazy val senderId: Option[Long] = Some(0L)
|
||||||
|
|
||||||
|
override lazy val modelValues: Future[Map[String, String]] =
|
||||||
|
Future.join(listName, listOwnerId).map {
|
||||||
|
case (nameOpt, authorId) =>
|
||||||
|
Map(
|
||||||
|
"list" -> listId.toString,
|
||||||
|
"list_name" -> nameOpt
|
||||||
|
.getOrElse(""),
|
||||||
|
"list_author" -> s"${authorId.getOrElse(0L)}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
package com.twitter.frigate.pushservice.model.ibis
|
||||||
|
|
||||||
|
import com.twitter.frigate.magic_events.thriftscala.CreatorFanoutType
|
||||||
|
import com.twitter.frigate.pushservice.model.PushTypes.PushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.model.MagicFanoutCreatorEventPushCandidate
|
||||||
|
import com.twitter.frigate.pushservice.util.PushIbisUtil.mergeModelValues
|
||||||
|
import com.twitter.util.Future
|
||||||
|
|
||||||
|
trait MagicFanoutCreatorEventIbis2Hydrator
|
||||||
|
extends CustomConfigurationMapForIbis
|
||||||
|
with Ibis2HydratorForCandidate {
|
||||||
|
self: PushCandidate with MagicFanoutCreatorEventPushCandidate =>
|
||||||
|
|
||||||
|
val userMap = Map(
|
||||||
|
"handle" -> userProfile.screenName,
|
||||||
|
"display_name" -> userProfile.name
|
||||||
|
)
|
||||||
|
|
||||||
|
override val senderId = hydratedCreator.map(_.id)
|
||||||
|
|
||||||
|
override lazy val modelValues: Future[Map[String, String]] =
|
||||||
|
mergeModelValues(super.modelValues, userMap)
|
||||||
|
|
||||||
|
override val ibis2Request = creatorFanoutType match {
|
||||||
|
case CreatorFanoutType.UserSubscription => Future.None
|
||||||
|
case CreatorFanoutType.NewCreator => super.ibis2Request
|
||||||
|
case _ => super.ibis2Request
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user