the-algorithm/pushservice/src/main/scala/com/twitter/frigate/pushservice/model/MagicFanoutHydratedCandidate.scala
twitter-team b389c3d302 Open-sourcing pushservice
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.
2023-05-19 16:27:07 -05:00

148 lines
5.3 KiB
Scala

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]])