mirror of
https://github.com/twitter/the-algorithm.git
synced 2024-06-01 08:48:46 +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.
153 lines
5.5 KiB
Scala
153 lines
5.5 KiB
Scala
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)
|
|
}
|
|
}
|
|
}
|