mirror of
https://github.com/twitter/the-algorithm.git
synced 2024-06-02 17:28:45 +02:00
b389c3d302
Pushservice is the main recommendation service we use to surface recommendations to our users via notifications. It fetches candidates from various sources, ranks them in order of relevance, and applies filters to determine the best one to send.
189 lines
7.5 KiB
Scala
189 lines
7.5 KiB
Scala
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)
|
|
}
|
|
}
|
|
}
|