From 14f78e176b9263ef4a14e58b896fd3af51a3bc1e Mon Sep 17 00:00:00 2001 From: dogemanttv <80775876+dogemanttv@users.noreply.github.com> Date: Wed, 10 Jan 2024 17:06:57 -0600 Subject: [PATCH] Delete follow-recommendations-service directory --- follow-recommendations-service/BUILD | 48 -- follow-recommendations-service/CONFIG.ini | 24 - .../FRS_architecture.png | Bin 181765 -> 0 bytes follow-recommendations-service/README.md | 40 -- .../follow_recommendations/common/base/BUILD | 18 - .../common/base/CandidateSourceRegistry.scala | 36 -- .../common/base/EnrichedCandidateSource.scala | 164 ------- .../common/base/ParamPredicate.scala | 17 - .../common/base/Predicate.scala | 282 ------------ .../common/base/PredicateResult.scala | 18 - .../common/base/Ranker.scala | 90 ---- .../common/base/RecommendationFlow.scala | 250 ----------- .../common/base/SideEffectsUtil.scala | 24 - .../common/base/StatsUtil.scala | 272 ----------- .../common/base/Transform.scala | 85 ---- .../addressbook/AddressBookParams.scala | 9 - .../candidate_sources/addressbook/BUILD | 27 -- .../addressbook/ForwardEmailBookSource.scala | 74 --- .../addressbook/ForwardPhoneBookSource.scala | 72 --- .../candidate_sources/addressbook/README.md | 4 - .../addressbook/ReverseEmailBookSource.scala | 78 ---- .../addressbook/ReversePhoneBookSource.scala | 77 ---- .../common/candidate_sources/base/BUILD | 23 - .../base/CachedCandidateSource.scala | 26 -- .../base/ExperimentalCandidateSource.scala | 66 --- .../base/RealGraphExpansionRepository.scala | 208 --------- .../base/SimilarUserExpanderParams.scala | 31 -- .../base/SimilarUserExpanderRepository.scala | 313 ------------- .../SocialProofEnforcedCandidateSource.scala | 86 ---- ...ProofEnforcedCandidateSourceFSConfig.scala | 30 -- ...alProofEnforcedCandidateSourceParams.scala | 56 --- .../base/StratoFetcherSource.scala | 27 -- .../StratoFetcherWithUnitViewSource.scala | 9 - .../base/TweetAuthorsCandidateSource.scala | 71 --- .../base/TwoHopExpansionCandidateSource.scala | 46 -- .../crowd_search_accounts/BUILD | 22 - .../CrowdSearchAccountsFSConfig.scala | 18 - .../CrowdSearchAccountsParams.scala | 32 -- .../CrowdSearchAccountsSource.scala | 111 ----- .../crowd_search_accounts/README.md | 4 - .../common/candidate_sources/geo/BUILD | 23 - .../geo/BasePopGeoHashSource.scala | 74 --- .../geo/PopCountryBackFillSource.scala | 33 -- .../geo/PopCountrySource.scala | 63 --- .../geo/PopGeoQualityFollowSource.scala | 99 ---- .../PopGeoQualityFollowSourceFSConfig.scala | 24 - .../geo/PopGeoQualityFollowSourceParams.scala | 42 -- .../candidate_sources/geo/PopGeoSource.scala | 69 --- .../geo/PopGeoSourceFSConfig.scala | 20 - .../geo/PopGeoSourceParams.scala | 30 -- .../geo/PopGeohashSource.scala | 36 -- .../common/candidate_sources/geo/README.md | 4 - .../ppmi_locale_follow/BUILD | 23 - .../PPMILocaleFollowSource.scala | 84 ---- .../PPMILocaleFollowSourceFSConfig.scala | 24 - .../PPMILocaleFollowSourceParams.scala | 22 - .../ppmi_locale_follow/README.md | 6 - .../candidate_sources/promoted_accounts/BUILD | 11 - .../PromotedAccountsCandidateSource.scala | 111 ----- .../promoted_accounts/README.md | 2 - .../common/candidate_sources/real_graph/BUILD | 24 - .../candidate_sources/real_graph/README.md | 6 - .../real_graph/RealGraphOonFSConfig.scala | 27 -- .../real_graph/RealGraphOonParams.scala | 47 -- .../real_graph/RealGraphOonV2Source.scala | 58 --- .../real_graph/RealGraphSource.scala | 40 -- .../candidate_sources/recent_engagement/BUILD | 29 -- .../recent_engagement/README.md | 4 - .../RecentEngagementDirectFollowSource.scala | 38 -- ...ecentEngagementNonDirectFollowSource.scala | 38 -- .../RepeatedProfileVisitsFSConfig.scala | 22 - .../RepeatedProfileVisitsParams.scala | 37 -- .../RepeatedProfileVisitsSource.scala | 157 ------- .../common/candidate_sources/salsa/BUILD | 21 - .../common/candidate_sources/salsa/README.md | 10 - ...mentDirectFollowSalsaExpansionSource.scala | 40 -- .../salsa/SalsaExpander.scala | 117 ----- .../SalsaExpansionBasedCandidateSource.scala | 32 -- .../common/candidate_sources/sims/BUILD | 24 - .../sims/CacheBasedSimsStore.scala | 50 --- .../sims/DBV2SimsRefreshStore.scala | 35 -- .../sims/DBV2SimsStore.scala | 38 -- .../Follow2vecNearestNeighborsStore.scala | 69 --- .../common/candidate_sources/sims/README.md | 32 -- .../sims/SimsExperimentalStore.scala | 36 -- .../sims/SimsSourceFSConfig.scala | 14 - .../sims/SimsSourceParams.scala | 16 - .../candidate_sources/sims/SimsStore.scala | 36 -- .../sims/StratoBasedSimsCandidateSource.scala | 40 -- ...BasedSimsCandidateSourceWithUnitView.scala | 10 - .../sims/SwitchingSimsSource.scala | 55 --- .../candidate_sources/sims_expansion/BUILD | 23 - .../DBV2SimsExpansionParams.scala | 22 - .../sims_expansion/README.md | 6 - ...RecentEngagementSimilarUsersFSConfig.scala | 14 - .../RecentEngagementSimilarUsersParams.scala | 17 - .../RecentEngagementSimilarUsersSource.scala | 113 ----- .../RecentFollowingSimilarUsersParams.scala | 29 -- .../RecentFollowingSimilarUsersSource.scala | 99 ---- ...gementDirectFollowSimilarUsersSource.scala | 53 --- .../SimsExpansionBasedCandidateSource.scala | 114 ----- .../SimsExpansionFSConfig.scala | 26 -- .../SimsExpansionSourceParams.scala | 17 - .../candidate_sources/socialgraph/BUILD | 18 - .../candidate_sources/socialgraph/README.md | 6 - ...lowingRecentFollowingExpansionSource.scala | 102 ----- ...centFollowingExpansionSourceFSConfig.scala | 16 - ...RecentFollowingExpansionSourceParams.scala | 10 - .../common/candidate_sources/stp/BUILD | 28 -- .../stp/BaseOnlineSTPSource.scala | 55 --- .../candidate_sources/stp/Dbv2StpScorer.scala | 30 -- .../candidate_sources/stp/EpStpScorer.scala | 65 --- ...utualFollowStrongTiePredictionSource.scala | 61 --- .../OfflineMutualFollowExpansionSource.scala | 23 - .../stp/OfflineStpSourceFsConfig.scala | 14 - .../stp/OfflineStpSourceParams.scala | 9 - .../OfflineStpSourceWithDensePmiMatrix.scala | 22 - .../OfflineStpSourceWithLegacyPmiMatrix.scala | 23 - ...OfflineStrongTiePredictionBaseSource.scala | 57 --- .../OfflineStrongTiePredictionSource.scala | 44 -- .../stp/OnlineSTPSourceFSConfig.scala | 15 - .../stp/OnlineSTPSourceParams.scala | 19 - .../stp/OnlineSTPSourceScorer.scala | 29 -- .../OnlineSTPSourceWithDeepbirdV2Scorer.scala | 76 ---- .../stp/OnlineSTPSourceWithEPScorer.scala | 58 --- .../common/candidate_sources/stp/README.md | 47 -- .../stp/STPFirstDegreeFetcher.scala | 155 ------- .../stp/STPGraphBuilder.scala | 32 -- .../stp/STPSecondDegreeFetcher.scala | 94 ---- ...rcedOfflineStrongTiePredictionSource.scala | 28 -- .../common/candidate_sources/stp/img.png | Bin 54417 -> 0 bytes .../top_organic_follows_accounts/BUILD | 22 - .../top_organic_follows_accounts/README.md | 2 - .../TopOrganicFollowsAccountsFSConfig.scala | 18 - .../TopOrganicFollowsAccountsParams.scala | 31 -- .../TopOrganicFollowsAccountsSource.scala | 110 ----- .../candidate_sources/triangular_loops/BUILD | 21 - .../triangular_loops/README.md | 5 - .../TriangularLoopsFSConfig.scala | 12 - .../TriangularLoopsParams.scala | 11 - .../TriangularLoopsSource.scala | 91 ---- .../two_hop_random_walk/BUILD | 20 - .../two_hop_random_walk/README.md | 7 - .../TwoHopRandomWalkSource.scala | 40 -- .../candidate_sources/user_user_graph/BUILD | 18 - .../user_user_graph/README.md | 4 - .../UserUserGraphCandidateSource.scala | 125 ------ .../UserUserGraphFSConfig.scala | 15 - .../user_user_graph/UserUserGraphParams.scala | 19 - .../addressbook/AddressbookClient.scala | 221 --------- .../addressbook/AddressbookModule.scala | 10 - .../common/clients/addressbook/BUILD | 21 - .../common/clients/addressbook/models/BUILD | 10 - .../clients/addressbook/models/Contact.scala | 29 -- .../clients/addressbook/models/EdgeType.scala | 16 - .../addressbook/models/QueryOption.scala | 24 - .../addressbook/models/RecordIdentifier.scala | 10 - .../common/clients/adserver/AdRequest.scala | 45 -- .../clients/adserver/AdserverClient.scala | 16 - .../clients/adserver/AdserverModule.scala | 15 - .../common/clients/adserver/BUILD | 14 - .../common/clients/cache/BUILD | 15 - .../common/clients/cache/MemcacheClient.scala | 121 ----- .../common/clients/cache/MemcacheModule.scala | 30 -- .../clients/cache/ThriftBijection.scala | 81 ---- .../common/clients/common/BUILD | 11 - .../clients/common/BaseClientModule.scala | 20 - .../common/clients/deepbirdv2/BUILD | 20 - ...pBirdV2PredictionServiceClientModule.scala | 67 --- .../common/clients/dismiss_store/BUILD | 19 - .../clients/dismiss_store/DismissStore.scala | 60 --- .../clients/email_storage_service/BUILD | 14 - .../EmailStorageServiceClient.scala | 28 -- .../EmailStorageServiceModule.scala | 12 - .../common/clients/geoduck/BUILD | 22 - .../geoduck/LocationServiceClient.scala | 62 --- .../geoduck/LocationServiceModule.scala | 12 - .../geoduck/ReverseGeocodeClient.scala | 57 --- .../clients/geoduck/UserLocationFetcher.scala | 59 --- .../common/clients/gizmoduck/BUILD | 21 - .../clients/gizmoduck/GizmoduckClient.scala | 81 ---- .../clients/gizmoduck/GizmoduckModule.scala | 24 - .../clients/graph_feature_service/BUILD | 14 - .../GraphFeatureServiceClient.scala | 50 --- .../GraphFeatureStoreModule.scala | 12 - .../common/clients/impression_store/BUILD | 18 - .../ImpressionStoreModule.scala | 31 -- .../impression_store/WtfImpressionStore.scala | 42 -- .../common/clients/interests_service/BUILD | 14 - .../InterestServiceClient.scala | 115 ----- .../clients/phone_storage_service/BUILD | 14 - .../PhoneStorageServiceClient.scala | 34 -- .../PhoneStorageServiceModule.scala | 12 - .../common/clients/real_time_real_graph/BUILD | 20 - .../real_time_real_graph/Engagement.scala | 14 - .../EngagementScorer.scala | 58 --- .../RealTimeRealGraphClient.scala | 128 ------ .../common/clients/socialgraph/BUILD | 26 -- .../socialgraph/SocialGraphClient.scala | 421 ------------------ .../socialgraph/SocialGraphModule.scala | 25 -- .../common/clients/strato/BUILD | 30 -- .../clients/strato/StratoClientModule.scala | 249 ----------- .../common/clients/user_state/BUILD | 17 - .../clients/user_state/UserStateClient.scala | 83 ---- .../common/constants/BUILD | 9 - .../CandidateAlgorithmTypeConstants.scala | 91 ---- .../constants/GuiceNamedConstants.scala | 43 -- .../common/constants/ServiceConstants.scala | 15 - .../common/feature_hydration/adapters/BUILD | 82 ---- .../adapters/CandidateAlgorithmAdapter.scala | 72 --- .../adapters/ClientContextAdapter.scala | 79 ---- .../adapters/PostNuxAlgorithmAdapter.scala | 151 ------- .../adapters/PreFetchedFeatureAdapter.scala | 91 ---- .../common/feature_hydration/common/BUILD | 18 - .../common/FeatureSource.scala | 23 - .../common/FeatureSourceId.scala | 19 - .../common/HasPreFetchedFeature.scala | 25 -- .../common/feature_hydration/sources/BUILD | 59 --- .../sources/CandidateAlgorithmSource.scala | 73 --- .../sources/ClientContextSource.scala | 43 -- .../FeatureHydrationSourcesFSConfig.scala | 42 -- ...ureHydrationSourcesFeatureSwitchKeys.scala | 42 -- .../sources/FeatureStoreFeatures.scala | 342 -------------- .../sources/FeatureStoreGizmoduckSource.scala | 188 -------- .../sources/FeatureStoreParameters.scala | 79 ---- .../FeatureStorePostNuxAlgorithmSource.scala | 232 ---------- .../sources/FeatureStoreSource.scala | 368 --------------- .../sources/FeatureStoreSourceParams.scala | 148 ------ .../FeatureStoreTimelinesAuthorSource.scala | 191 -------- .../FeatureStoreUserMetricCountsSource.scala | 187 -------- .../sources/HydrationSourcesModule.scala | 152 ------- .../sources/PreFetchedFeatureSource.scala | 36 -- .../sources/UserScoringFeatureSource.scala | 86 ---- .../feature_hydration/sources/Utils.scala | 30 -- .../common/features/BUILD | 9 - .../common/features/LocationFeature.scala | 10 - .../features/TrackingTokenFeature.scala | 8 - .../common/features/UserStateFeature.scala | 7 - .../common/models/AddressBookMetadata.scala | 29 -- .../common/models/AlgorithmType.scala | 20 - .../common/models/BUILD | 29 -- .../common/models/CandidateUser.scala | 192 -------- .../models/ClientContextConverter.scala | 53 --- .../common/models/DisplayLocation.scala | 420 ----------------- .../common/models/EngagementType.scala | 62 --- .../common/models/FilterReason.scala | 133 ------ .../common/models/FlowContext.scala | 20 - .../common/models/FlowRecommendation.scala | 23 - .../common/models/GeohashAndCountryCode.scala | 3 - .../common/models/HasAdMetadata.scala | 23 - .../common/models/HasByfSeedUserIds.scala | 5 - .../common/models/HasDataRecord.scala | 86 ---- .../common/models/HasDebugOptions.scala | 30 -- .../common/models/HasDismissedUserIds.scala | 6 - .../common/models/HasDisplayLocation.scala | 5 - .../common/models/HasEngagements.scala | 7 - .../common/models/HasExcludedUserIds.scala | 6 - .../models/HasGeohashAndCountryCode.scala | 5 - .../models/HasInfoPerRankingStage.scala | 5 - .../common/models/HasInterestIds.scala | 11 - .../HasInvalidRelationshipUserIds.scala | 6 - .../common/models/HasIsSoftUser.scala | 5 - .../models/HasMutualFollowedUserIds.scala | 10 - .../HasPreviousRecommendationsContext.scala | 12 - .../common/models/HasProfileId.scala | 5 - .../common/models/HasQualityFactor.scala | 5 - .../models/HasRecentFollowedByUserIds.scala | 8 - .../models/HasRecentFollowedUserIds.scala | 14 - .../HasRecentFollowedUserIdsWithTime.scala | 9 - .../models/HasRecentlyEngagedUserIds.scala | 5 - .../HasRecommendationFlowIdentifier.scala | 5 - .../common/models/HasScores.scala | 5 - .../common/models/HasSimilarToContext.scala | 7 - .../common/models/HasTopicId.scala | 5 - .../HasUserCandidateSourceDetails.scala | 162 ------- .../common/models/HasUserState.scala | 7 - .../common/models/HasWtfImpressions.scala | 30 -- .../common/models/OptimusRequest.scala | 15 - .../common/models/Product.scala | 15 - .../common/models/RankingInfo.scala | 28 -- .../common/models/Reason.scala | 206 --------- .../common/models/RecentlyEngagedUserId.scala | 31 -- .../common/models/RecommendationStep.scala | 30 -- .../common/models/STPGraph.scala | 22 - .../common/models/SafetyLevel.scala | 17 - .../common/models/Score.scala | 144 ------ .../common/models/Session.scala | 16 - .../common/models/SignalData.scala | 42 -- .../common/models/TrackingToken.scala | 62 --- .../common/models/TweetCandidate.scala | 6 - .../models/UserCandidateSourceDetails.scala | 97 ---- .../common/models/UserIdAndTimestamp.scala | 3 - .../common/models/WtfImpression.scala | 12 - .../common/predicates/BUILD | 21 - .../predicates/CandidateParamPredicate.scala | 21 - .../CandidateSourceParamPredicate.scala | 31 -- .../CuratedCompetitorListPredicate.scala | 66 --- .../predicates/ExcludedUserIdPredicate.scala | 24 - .../common/predicates/InactivePredicate.scala | 121 ----- .../predicates/InactivePredicateParams.scala | 21 - ...reviouslyRecommendedUserIdsPredicate.scala | 34 -- .../common/predicates/dismiss/BUILD | 17 - .../dismiss/DismissedCandidatePredicate.scala | 32 -- .../DismissedCandidatePredicateParams.scala | 9 - .../common/predicates/gizmoduck/BUILD | 23 - .../gizmoduck/GizmoduckPredicate.scala | 284 ------------ .../gizmoduck/GizmoduckPredicateCache.scala | 50 --- .../GizmoduckPredicateFSConfig.scala | 17 - .../gizmoduck/GizmoduckPredicateParams.scala | 21 - .../common/predicates/health/BUILD | 21 - .../predicates/health/HssPredicate.scala | 95 ---- .../health/HssPredicateFSConfig.scala | 22 - .../health/HssPredicateParams.scala | 34 -- .../common/predicates/sgs/BUILD | 19 - .../sgs/InvalidRelationshipPredicate.scala | 36 -- .../sgs/RecentFollowingPredicate.scala | 33 -- .../predicates/sgs/SgsPredicateFSConfig.scala | 16 - .../predicates/sgs/SgsPredicateParams.scala | 19 - .../SgsRelationshipsByUserIdPredicate.scala | 113 ----- .../sgs/SgsRelationshipsPredicate.scala | 146 ------ .../common/predicates/user_activity/BUILD | 20 - .../user_activity/UserActivityPredicate.scala | 161 ------- .../UserActivityPredicateParams.scala | 10 - .../common/AdhocScoreModificationType.scala | 20 - .../common/rankers/common/BUILD | 10 - .../rankers/common/DedupCandidates.scala | 11 - .../common/rankers/common/RankerId.scala | 27 -- .../common/rankers/fatigue_ranker/BUILD | 13 - .../ImpressionBasedFatigueRanker.scala | 141 ------ ...ImpressionBasedFatigueRankerFSConfig.scala | 12 - .../ImpressionBasedFatigueRankerParams.scala | 14 - .../common/rankers/first_n_ranker/BUILD | 20 - .../rankers/first_n_ranker/FirstNRanker.scala | 115 ----- .../first_n_ranker/FirstNRankerFSConfig.scala | 21 - .../FirstNRankerFeatureSwitchKeys.scala | 8 - .../first_n_ranker/FirstNRankerParams.scala | 26 -- .../common/rankers/interleave_ranker/BUILD | 21 - .../interleave_ranker/InterleaveRanker.scala | 204 --------- .../InterleaveRankerFSConfig.scala | 12 - .../InterleaveRankerParams.scala | 8 - .../common/rankers/ml_ranker/ranking/BUILD | 37 -- .../ranking/HydrateFeaturesTransform.scala | 57 --- .../rankers/ml_ranker/ranking/MlRanker.scala | 219 --------- .../ml_ranker/ranking/MlRankerFSConfig.scala | 12 - .../ml_ranker/ranking/MlRankerParams.scala | 30 -- .../ml_ranker/scoring/AdhocScorer.scala | 28 -- .../common/rankers/ml_ranker/scoring/BUILD | 23 - .../ml_ranker/scoring/DeepbirdScorer.scala | 151 ------- .../scoring/PostnuxDeepbirdProdScorer.scala | 34 -- .../ml_ranker/scoring/RandomScorer.scala | 42 -- .../rankers/ml_ranker/scoring/Scorer.scala | 34 -- .../ml_ranker/scoring/ScorerFactory.scala | 38 -- .../common/rankers/utils/BUILD | 8 - .../common/rankers/utils/Utils.scala | 28 -- .../weighted_candidate_source_ranker/BUILD | 20 - .../CandidateShuffle.scala | 36 -- .../WeightMethod.scala | 6 - .../WeightedCandidateSourceBaseRanker.scala | 118 ----- .../WeightedCandidateSourceRanker.scala | 100 ----- ...eightedCandidateSourceRankerFSConfig.scala | 13 - .../WeightedCandidateSourceRankerParams.scala | 8 - .../common/stores/BUILD | 19 - .../stores/LowTweepCredFollowStore.scala | 39 -- .../common/transforms/dedup/BUILD | 8 - .../transforms/dedup/DedupTransform.scala | 14 - .../transforms/modify_social_proof/BUILD | 22 - .../ModifySocialProofTransform.scala | 202 --------- .../RemoveAccountProofTransform.scala | 27 -- .../common/transforms/ranker_id/BUILD | 19 - .../ranker_id/RandomRankerIdTransform.scala | 24 - ...ecommendationFlowIdentifierTransform.scala | 20 - .../recommendation_flow_identifier/BUILD | 9 - .../common/transforms/tracking_token/BUILD | 18 - .../TrackingTokenTransform.scala | 76 ---- .../common/transforms/weighted_sampling/BUILD | 10 - .../weighted_sampling/SamplingTransform.scala | 138 ------ .../SamplingTransformFSConfig.scala | 19 - .../SamplingTransformParams.scala | 25 -- .../follow_recommendations/common/utils/BUILD | 13 - .../common/utils/CollectionUtil.scala | 22 - .../DisplayLocationProductConverterUtil.scala | 27 -- .../common/utils/MergeUtil.scala | 51 --- .../common/utils/RandomUtil.scala | 88 ---- .../common/utils/RescueWithStatsUtils.scala | 50 --- .../common/utils/UserSignupUtil.scala | 14 - .../common/utils/Weighted.scala | 21 - .../server/src/main/resources/BUILD | 20 - .../src/main/resources/config/decider.yml | 129 ------ .../server/src/main/resources/logback.xml | 133 ------ .../quality/stp_models/20141223/epModel | 8 - .../stp_models/20141223/trainingConfig | 1 - .../com/twitter/follow_recommendations/BUILD | 48 -- ...owRecommendationsServiceThriftServer.scala | 118 ----- .../assembler/models/Action.scala | 9 - .../assembler/models/BUILD | 12 - .../assembler/models/Config.scala | 8 - .../assembler/models/FeedbackAction.scala | 13 - .../assembler/models/Footer.scala | 9 - .../assembler/models/Header.scala | 9 - .../assembler/models/Layout.scala | 16 - .../models/RecommendationOptions.scala | 11 - .../assembler/models/SocialProof.scala | 16 - .../assembler/models/Title.scala | 9 - .../assembler/models/WTFPresentation.scala | 47 -- .../follow_recommendations/blenders/BUILD | 16 - .../blenders/PromotedAccountsBlender.scala | 138 ------ .../follow_recommendations/configapi/BUILD | 28 -- .../configapi/ConfigBuilder.scala | 16 - .../configapi/DeciderConfigs.scala | 52 --- .../configapi/FeatureSwitchConfigs.scala | 138 ------ .../configapi/GlobalFeatureSwitchConfig.scala | 49 -- .../configapi/ParamsFactory.scala | 29 -- .../configapi/RequestContext.scala | 19 - .../configapi/RequestContextFactory.scala | 74 --- .../configapi/candidates/BUILD | 18 - .../candidates/CandidateUserContext.scala | 19 - .../CandidateUserContextFactory.scala | 55 --- .../CandidateUserParamsFactory.scala | 35 -- .../HydrateCandidateParamsTransform.scala | 21 - .../configapi/common/BUILD | 8 - .../common/FeatureSwitchConfig.scala | 60 --- .../configapi/deciders/BUILD | 10 - .../configapi/deciders/DeciderKey.scala | 51 --- .../configapi/deciders/DeciderParams.scala | 8 - .../configapi/params/BUILD | 13 - .../configapi/params/GlobalParams.scala | 35 -- .../follow_recommendations/controllers/BUILD | 29 -- .../CandidateUserDebugParamsBuilder.scala | 25 -- .../RecommendationRequestBuilder.scala | 41 -- .../RequestBuilderUserFetcher.scala | 48 -- .../ScoringUserRequestBuilder.scala | 53 --- .../controllers/ThriftController.scala | 41 -- .../follow_recommendations/flows/ads/BUILD | 19 - .../flows/ads/PromotedAccountsFlow.scala | 112 ----- .../ads/PromotedAccountsFlowParams.scala | 19 - .../ads/PromotedAccountsFlowRequest.scala | 33 -- .../flows/ads/PromotedAccountsUtil.scala | 28 -- .../flows/content_recommender_flow/BUILD | 32 -- .../ContentRecommenderFlow.scala | 202 --------- ...commenderFlowCandidateSourceRegistry.scala | 78 ---- ...ecommenderFlowCandidateSourceWeights.scala | 71 --- ...nderFlowCandidateSourceWeightsParams.scala | 117 ----- .../ContentRecommenderFlowFSConfig.scala | 60 --- ...tentRecommenderFlowFeatureSwitchKeys.scala | 70 --- .../ContentRecommenderParams.scala | 85 ---- .../ContentRecommenderRequest.scala | 45 -- .../ContentRecommenderRequestBuilder.scala | 121 ----- .../flows/post_nux_ml/BUILD | 58 --- .../PostNuxMlCandidateSourceRegistry.scala | 103 ----- ...PostNuxMlCandidateSourceWeightParams.scala | 177 -------- .../PostNuxMlCombinedRankerBuilder.scala | 193 -------- .../flows/post_nux_ml/PostNuxMlFlow.scala | 304 ------------- .../PostNuxMlFlowCandidateSourceWeights.scala | 68 --- ...didateSourceWeightsFeatureSwitchKeys.scala | 46 -- .../post_nux_ml/PostNuxMlFlowFSConfig.scala | 80 ---- .../PostNuxMlFlowFeatureSwitchKeys.scala | 27 -- .../flows/post_nux_ml/PostNuxMlParams.scala | 133 ------ .../flows/post_nux_ml/PostNuxMlRequest.scala | 54 --- .../post_nux_ml/PostNuxMlRequestBuilder.scala | 173 ------- .../PostNuxMlRequestBuilderParams.scala | 45 -- .../follow_recommendations/logging/BUILD | 18 - .../logging/FrsLogger.scala | 164 ------- .../follow_recommendations/models/BUILD | 13 - .../models/CandidateSourceType.scala | 9 - .../models/CandidateUserDebugParams.scala | 5 - .../models/DebugParams.scala | 28 -- .../models/DisplayContext.scala | 113 ----- .../models/FeatureValue.scala | 24 - .../models/RecommendationFlowData.scala | 104 ----- .../models/RecommendationRequest.scala | 29 -- .../models/RecommendationResponse.scala | 14 - .../models/Request.scala | 22 - .../models/ScoringUserRequest.scala | 45 -- .../models/ScoringUserResponse.scala | 15 - .../models/failures/BUILD | 8 - .../failures/TimeoutPipelineFailure.scala | 12 - .../modules/ABDeciderModule.scala | 31 -- .../follow_recommendations/modules/BUILD | 24 - .../modules/ConfigApiModule.scala | 20 - .../modules/DiffyModule.scala | 71 --- .../modules/FeatureSwitchesModule.scala | 85 ---- .../modules/FlagsModule.scala | 18 - .../modules/ProductRegistryModule.scala | 12 - .../modules/ScorerModule.scala | 40 -- .../modules/ScribeModule.scala | 95 ---- .../modules/TimerModule.scala | 13 - .../follow_recommendations/products/BUILD | 16 - .../products/ProdProductRegistry.scala | 44 -- .../products/common/BUILD | 12 - .../products/common/Exceptions.scala | 7 - .../products/common/Product.scala | 56 --- .../products/common/ProductRegistry.scala | 9 - .../products/explore_tab/BUILD | 14 - .../explore_tab/ExploreTabProduct.scala | 50 --- .../products/explore_tab/configapi/BUILD | 9 - .../configapi/ExploreTabFSConfig.scala | 14 - .../configapi/ExploreTabParams.scala | 10 - .../products/home_timeline/BUILD | 14 - .../home_timeline/HTLProductMixer.scala | 9 - .../home_timeline/HomeTimelineProduct.scala | 114 ----- .../home_timeline/HomeTimelineStrings.scala | 26 -- .../products/home_timeline/configapi/BUILD | 9 - .../configapi/HomeTimelineFSConfig.scala | 22 - .../configapi/HomeTimelineParams.scala | 38 -- .../products/home_timeline_tweet_recs/BUILD | 13 - .../HomeTimelineTweetRecsProduct.scala | 50 --- .../home_timeline_tweet_recs/configapi/BUILD | 10 - .../HomeTimelineTweetRecsParams.scala | 7 - .../products/sidebar/BUILD | 14 - .../products/sidebar/SidebarProduct.scala | 73 --- .../products/sidebar/configapi/BUILD | 8 - .../sidebar/configapi/SidebarParams.scala | 9 - .../follow_recommendations/services/BUILD | 34 -- ...wRecommendationsServiceWarmupHandler.scala | 101 ----- .../ProductMixerRecommendationService.scala | 72 --- .../services/ProductPipelineSelector.scala | 188 -------- .../ProductPipelineSelectorConfig.scala | 19 - .../services/ProductRecommenderService.scala | 72 --- .../services/RecommendationsService.scala | 28 -- .../services/UserScoringService.scala | 84 ---- .../services/exceptions/BUILD | 14 - .../exceptions/UnknownExceptionMapper.scala | 18 - .../follow_recommendations/utils/BUILD | 29 -- .../utils/CandidateSourceHoldbackUtil.scala | 82 ---- ...ecommendationFlowBaseSideEffectsUtil.scala | 121 ----- .../thrift/src/main/thrift/BUILD | 21 - .../thrift/src/main/thrift/assembler.thrift | 42 -- .../src/main/thrift/client_context.thrift | 19 - .../thrift/src/main/thrift/debug.thrift | 73 --- .../src/main/thrift/display_context.thrift | 62 --- .../src/main/thrift/display_location.thrift | 55 --- .../src/main/thrift/engagementType.thrift | 11 - .../thrift/src/main/thrift/flows.thrift | 20 - .../follow-recommendations-service.thrift | 100 ----- ...low_recommendations_serving_history.thrift | 9 - .../thrift/src/main/thrift/logging/BUILD | 18 - .../main/thrift/logging/client_context.thrift | 14 - .../src/main/thrift/logging/debug.thrift | 8 - .../thrift/logging/display_context.thrift | 66 --- .../thrift/logging/display_location.thrift | 55 --- .../main/thrift/logging/engagementType.thrift | 11 - .../src/main/thrift/logging/flows.thrift | 16 - .../src/main/thrift/logging/logs.thrift | 72 --- .../src/main/thrift/logging/reasons.thrift | 62 --- .../logging/recently_engaged_user_id.thrift | 10 - .../thrift/logging/recommendations.thrift | 26 -- .../src/main/thrift/logging/scoring.thrift | 38 -- .../src/main/thrift/logging/tracking.thrift | 16 - .../thrift/src/main/thrift/reasons.thrift | 61 --- .../thrift/recently_engaged_user_id.thrift | 10 - .../src/main/thrift/recommendations.thrift | 40 -- .../thrift/src/main/thrift/scoring.thrift | 49 -- .../thrift/src/main/thrift/tracking.thrift | 17 - 553 files changed, 27180 deletions(-) delete mode 100644 follow-recommendations-service/BUILD delete mode 100644 follow-recommendations-service/CONFIG.ini delete mode 100644 follow-recommendations-service/FRS_architecture.png delete mode 100644 follow-recommendations-service/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/CandidateSourceRegistry.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/EnrichedCandidateSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/ParamPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Predicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/PredicateResult.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Ranker.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/RecommendationFlow.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/SideEffectsUtil.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/StatsUtil.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Transform.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/AddressBookParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ForwardEmailBookSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ForwardPhoneBookSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ReverseEmailBookSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ReversePhoneBookSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/CachedCandidateSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/ExperimentalCandidateSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/RealGraphExpansionRepository.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SimilarUserExpanderParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SimilarUserExpanderRepository.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSourceFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSourceParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/StratoFetcherSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/StratoFetcherWithUnitViewSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/TweetAuthorsCandidateSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/TwoHopExpansionCandidateSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/BasePopGeoHashSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopCountryBackFillSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopCountrySource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSourceFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSourceParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSourceFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSourceParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeohashSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSourceFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSourceParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/PromotedAccountsCandidateSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonV2Source.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RecentEngagementDirectFollowSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RecentEngagementNonDirectFollowSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/RecentEngagementDirectFollowSalsaExpansionSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/SalsaExpander.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/SalsaExpansionBasedCandidateSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/CacheBasedSimsStore.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/DBV2SimsRefreshStore.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/DBV2SimsStore.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/Follow2vecNearestNeighborsStore.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsExperimentalStore.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsSourceFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsSourceParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsStore.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/StratoBasedSimsCandidateSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/StratoBasedSimsCandidateSourceWithUnitView.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SwitchingSimsSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/DBV2SimsExpansionParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentFollowingSimilarUsersParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentFollowingSimilarUsersSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentStrongEngagementDirectFollowSimilarUsersSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionBasedCandidateSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionSourceParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSourceFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSourceParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/BaseOnlineSTPSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/Dbv2StpScorer.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/EpStpScorer.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/MutualFollowStrongTiePredictionSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineMutualFollowExpansionSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceFsConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceWithDensePmiMatrix.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceWithLegacyPmiMatrix.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStrongTiePredictionBaseSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStrongTiePredictionSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceScorer.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceWithDeepbirdV2Scorer.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceWithEPScorer.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPFirstDegreeFetcher.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPGraphBuilder.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPSecondDegreeFetcher.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/SocialProofEnforcedOfflineStrongTiePredictionSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/img.png delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/top_organic_follows_accounts/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/top_organic_follows_accounts/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/top_organic_follows_accounts/TopOrganicFollowsAccountsFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/top_organic_follows_accounts/TopOrganicFollowsAccountsParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/top_organic_follows_accounts/TopOrganicFollowsAccountsSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/TwoHopRandomWalkSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/README.md delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphCandidateSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/AddressbookClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/AddressbookModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/Contact.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/EdgeType.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/QueryOption.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/RecordIdentifier.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdRequest.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdserverClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdserverModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/MemcacheClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/MemcacheModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/ThriftBijection.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common/BaseClientModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2/DeepBirdV2PredictionServiceClientModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/dismiss_store/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/dismiss_store/DismissStore.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/EmailStorageServiceClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/EmailStorageServiceModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/LocationServiceClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/LocationServiceModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/ReverseGeocodeClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/UserLocationFetcher.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/GizmoduckClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/GizmoduckModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/GraphFeatureServiceClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/GraphFeatureStoreModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/ImpressionStoreModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/WtfImpressionStore.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/interests_service/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/interests_service/InterestServiceClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/PhoneStorageServiceClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/PhoneStorageServiceModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/Engagement.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/EngagementScorer.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/RealTimeRealGraphClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/SocialGraphClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/SocialGraphModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato/StratoClientModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/user_state/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/user_state/UserStateClient.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/CandidateAlgorithmTypeConstants.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/GuiceNamedConstants.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/ServiceConstants.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/CandidateAlgorithmAdapter.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/ClientContextAdapter.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/PostNuxAlgorithmAdapter.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/PreFetchedFeatureAdapter.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/FeatureSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/FeatureSourceId.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/HasPreFetchedFeature.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/CandidateAlgorithmSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/ClientContextSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureHydrationSourcesFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureHydrationSourcesFeatureSwitchKeys.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreFeatures.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreGizmoduckSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreParameters.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStorePostNuxAlgorithmSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreSourceParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreTimelinesAuthorSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreUserMetricCountsSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/HydrationSourcesModule.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/PreFetchedFeatureSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/UserScoringFeatureSource.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/Utils.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/LocationFeature.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/TrackingTokenFeature.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/UserStateFeature.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/AddressBookMetadata.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/AlgorithmType.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/CandidateUser.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/ClientContextConverter.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/DisplayLocation.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/EngagementType.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FilterReason.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FlowContext.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FlowRecommendation.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/GeohashAndCountryCode.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasAdMetadata.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasByfSeedUserIds.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDataRecord.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDebugOptions.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDismissedUserIds.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDisplayLocation.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasEngagements.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasExcludedUserIds.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasGeohashAndCountryCode.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInfoPerRankingStage.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInterestIds.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInvalidRelationshipUserIds.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasIsSoftUser.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasMutualFollowedUserIds.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasPreviousRecommendationsContext.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasProfileId.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasQualityFactor.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedByUserIds.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedUserIds.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedUserIdsWithTime.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentlyEngagedUserIds.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecommendationFlowIdentifier.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasScores.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasSimilarToContext.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasTopicId.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasUserCandidateSourceDetails.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasUserState.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasWtfImpressions.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/OptimusRequest.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Product.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RankingInfo.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Reason.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RecentlyEngagedUserId.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RecommendationStep.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/STPGraph.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/SafetyLevel.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Score.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Session.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/SignalData.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/TrackingToken.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/TweetCandidate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/UserCandidateSourceDetails.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/UserIdAndTimestamp.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/WtfImpression.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CandidateParamPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CandidateSourceParamPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CuratedCompetitorListPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/ExcludedUserIdPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/InactivePredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/InactivePredicateParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/PreviouslyRecommendedUserIdsPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/DismissedCandidatePredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/DismissedCandidatePredicateParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateCache.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicateFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicateParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/InvalidRelationshipPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/RecentFollowingPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsPredicateFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsPredicateParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsRelationshipsByUserIdPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsRelationshipsPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/UserActivityPredicate.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/UserActivityPredicateParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/AdhocScoreModificationType.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/DedupCandidates.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/RankerId.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRanker.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRankerFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRankerParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRanker.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerFeatureSwitchKeys.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRanker.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRankerFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRankerParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/HydrateFeaturesTransform.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRanker.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRankerFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRankerParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/AdhocScorer.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/DeepbirdScorer.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/PostnuxDeepbirdProdScorer.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/RandomScorer.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/Scorer.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/ScorerFactory.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils/Utils.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/CandidateShuffle.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightMethod.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceBaseRanker.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRanker.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRankerFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRankerParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/stores/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/stores/LowTweepCredFollowStore.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/dedup/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/dedup/DedupTransform.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/ModifySocialProofTransform.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/RemoveAccountProofTransform.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/ranker_id/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/ranker_id/RandomRankerIdTransform.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/recommendation_flow_identifier/AddRecommendationFlowIdentifierTransform.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/recommendation_flow_identifier/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/tracking_token/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/tracking_token/TrackingTokenTransform.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransform.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransformFSConfig.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransformParams.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/BUILD delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/CollectionUtil.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/DisplayLocationProductConverterUtil.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/MergeUtil.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/RandomUtil.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/RescueWithStatsUtils.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/UserSignupUtil.scala delete mode 100644 follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/Weighted.scala delete mode 100644 follow-recommendations-service/server/src/main/resources/BUILD delete mode 100644 follow-recommendations-service/server/src/main/resources/config/decider.yml delete mode 100644 follow-recommendations-service/server/src/main/resources/logback.xml delete mode 100644 follow-recommendations-service/server/src/main/resources/quality/stp_models/20141223/epModel delete mode 100644 follow-recommendations-service/server/src/main/resources/quality/stp_models/20141223/trainingConfig delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/FollowRecommendationsServiceThriftServer.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Action.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Config.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/FeedbackAction.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Footer.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Header.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Layout.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/RecommendationOptions.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/SocialProof.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Title.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/WTFPresentation.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders/PromotedAccountsBlender.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/ConfigBuilder.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/DeciderConfigs.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/FeatureSwitchConfigs.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/GlobalFeatureSwitchConfig.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/ParamsFactory.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/RequestContext.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/RequestContextFactory.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserContext.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserContextFactory.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserParamsFactory.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/HydrateCandidateParamsTransform.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common/FeatureSwitchConfig.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/DeciderKey.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/DeciderParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params/GlobalParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/CandidateUserDebugParamsBuilder.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/RecommendationRequestBuilder.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/RequestBuilderUserFetcher.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/ScoringUserRequestBuilder.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/ThriftController.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlow.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlowParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlowRequest.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsUtil.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlow.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceRegistry.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceWeights.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceWeightsParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowFSConfig.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowFeatureSwitchKeys.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderRequest.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderRequestBuilder.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCandidateSourceRegistry.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCandidateSourceWeightParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCombinedRankerBuilder.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlow.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowCandidateSourceWeights.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowFSConfig.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowFeatureSwitchKeys.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequest.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequestBuilder.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequestBuilderParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/logging/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/logging/FrsLogger.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/CandidateSourceType.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/CandidateUserDebugParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/DebugParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/DisplayContext.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/FeatureValue.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationFlowData.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationRequest.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationResponse.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/Request.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/ScoringUserRequest.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/ScoringUserResponse.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/failures/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/failures/TimeoutPipelineFailure.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ABDeciderModule.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ConfigApiModule.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/DiffyModule.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/FeatureSwitchesModule.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/FlagsModule.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ProductRegistryModule.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ScorerModule.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ScribeModule.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/TimerModule.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/ProdProductRegistry.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/Exceptions.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/Product.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/ProductRegistry.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/ExploreTabProduct.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/ExploreTabFSConfig.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/ExploreTabParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HTLProductMixer.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HomeTimelineProduct.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HomeTimelineStrings.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/HomeTimelineFSConfig.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/HomeTimelineParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/HomeTimelineTweetRecsProduct.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/configapi/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/configapi/HomeTimelineTweetRecsParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/SidebarProduct.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/configapi/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/configapi/SidebarParams.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/FollowRecommendationsServiceWarmupHandler.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductMixerRecommendationService.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductPipelineSelector.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductPipelineSelectorConfig.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductRecommenderService.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/RecommendationsService.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/UserScoringService.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/exceptions/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/exceptions/UnknownExceptionMapper.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/BUILD delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/CandidateSourceHoldbackUtil.scala delete mode 100644 follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/RecommendationFlowBaseSideEffectsUtil.scala delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/BUILD delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/assembler.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/client_context.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/debug.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/display_context.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/display_location.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/engagementType.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/flows.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/follow-recommendations-service.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/follow_recommendations_serving_history.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/BUILD delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/client_context.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/debug.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/display_context.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/display_location.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/engagementType.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/flows.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/logs.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/reasons.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/recently_engaged_user_id.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/recommendations.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/scoring.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/logging/tracking.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/reasons.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/recently_engaged_user_id.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/recommendations.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/scoring.thrift delete mode 100644 follow-recommendations-service/thrift/src/main/thrift/tracking.thrift diff --git a/follow-recommendations-service/BUILD b/follow-recommendations-service/BUILD deleted file mode 100644 index 5210cbd88..000000000 --- a/follow-recommendations-service/BUILD +++ /dev/null @@ -1,48 +0,0 @@ -# Without this alias, library :follow-recommendations-service_lib would conflict with :bin -alias( - name = "follow-recommendations-service", - target = ":follow-recommendations-service_lib", -) - -target( - name = "follow-recommendations-service_lib", - dependencies = [ - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models", - ], -) - -jvm_binary( - name = "bin", - basename = "follow-recommendations-service", - main = "com.twitter.follow_recommendations.FollowRecommendationsServiceThriftServerMain", - runtime_platform = "java11", - tags = ["bazel-compatible"], - dependencies = [ - ":follow-recommendations-service", - "3rdparty/jvm/ch/qos/logback:logback-classic", - "finagle/finagle-zipkin-scribe/src/main/scala", - "finatra/inject/inject-logback/src/main/scala", - "loglens/loglens-logback/src/main/scala/com/twitter/loglens/logback", - "twitter-server-internal/src/main/scala", - "twitter-server/logback-classic/src/main/scala", - ], -) - -# Aurora Workflows build phase convention requires a jvm_app named with ${project-name}-app -jvm_app( - name = "follow-recommendations-service-app", - archive = "zip", - binary = ":bin", - bundles = [ - bundle( - fileset = [ - "server/src/main/resources/*", - "server/src/main/resources/**/*", - ], - owning_target = "follow-recommendations-service/server/src/main/resources:frs_resources", - relative_to = "server/src/main/resources", - ), - ], - tags = ["bazel-compatible"], -) diff --git a/follow-recommendations-service/CONFIG.ini b/follow-recommendations-service/CONFIG.ini deleted file mode 100644 index c8173efa8..000000000 --- a/follow-recommendations-service/CONFIG.ini +++ /dev/null @@ -1,24 +0,0 @@ -[code-coverage] -package = com.twitter.follow_recommendations - -[docbird] -project_name = follow-recommendations-service -project_type = service -; example settings: -; -; project_name = fluffybird -; description = fluffybird is a service for fluffing up feathers. -; tags = python,documentation,fluffybird -; project_type = service -; - allowed options: essay, library, service, hub, cookbook, styleguide, policy -; owner_links = roster -; - allowed options: roster, find, email -; scrolling_tocs = yes -; comments = yes -; verifications = yes -; support_widget = yes -; health_score = yes -; sticky_sidebar = no - -[jira] -project = CJREL diff --git a/follow-recommendations-service/FRS_architecture.png b/follow-recommendations-service/FRS_architecture.png deleted file mode 100644 index eda105e45ebc9bd6236d88b9d6917a64a58d1f5a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 181765 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

$thr|Q+9VR=-zz>AU4G9q+=#B*T+kUb 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

Y+jP>&HD>c-p>pHtKF6>V{UDYR*51|! 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 LRhB7{G7kPfdsbKE diff --git a/follow-recommendations-service/README.md b/follow-recommendations-service/README.md deleted file mode 100644 index 1640184a5..000000000 --- a/follow-recommendations-service/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# Follow Recommendations Service - -## Introduction to the Follow Recommendations Service (FRS) -The Follow Recommendations Service (FRS) is a robust recommendation engine designed to provide users with personalized suggestions for accounts to follow. At present, FRS supports Who-To-Follow (WTF) module recommendations across a variety of Twitter product interfaces. Additionally, by suggesting tweet authors, FRS also delivers FutureGraph tweet recommendations, which consist of tweets from accounts that users may be interested in following in the future. - -## Design -The system is tailored to accommodate diverse use cases, such as Post New-User-Experience (NUX), advertisements, FutureGraph tweets, and more. Each use case features a unique display location identifier. To view all display locations, refer to the following path: `follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/DisplayLocation.scala`. - -Recommendation steps are customized according to each display location. Common and high-level steps are encapsulated within the "RecommendationFlow," which includes operations like candidate generation, ranker selection, filtering, transformation, and beyond. To explore all flows, refer to this path: `follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows`. - -For each product (corresponding to a display location), one or multiple flows can be selected to generate candidates based on code and configurations. To view all products, refer to the following path: `follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs`. - -The FRS overview diagram is depicted below: - -![FRS_architecture.png](FRS_architecture.png) - - -### Candidate Generation -During this step, FRS utilizes various user signals and algorithms to identify candidates from all Twitter accounts. The candidate source folder is located at `follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/`, with a README file provided within each candidate source folder. - -### Filtering -In this phase, FRS applies different filtering logic after generating account candidates to improve quality and health. Filtering may occur before and/or after the ranking step, with heavier filtering logic (e.g., higher latency) typically applied after the ranking step. The filters' folder is located at `follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates`. - -### Ranking -During this step, FRS employs both Machine Learning (ML) and heuristic rule-based candidate ranking. For the ML ranker, ML features are fetched beforehand (i.e., feature hydration), -and a DataRecord (the Twitter-standard Machine Learning data format used to represent feature data, labels, and predictions when training or serving) is constructed for each pair. -These pairs are then sent to a separate ML prediction service, which houses the ML model trained offline. -The ML prediction service returns a prediction score, representing the probability that a user will follow and engage with the candidate. -This score is a weighted sum of p(follow|recommendation) and p(positive engagement|follow), and FRS uses this score to rank the candidates. - -The rankers' folder is located at `follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers`. - -### Transform -In this phase, the sequence of candidates undergoes necessary transformations, such as deduplication, attaching social proof (i.e., "followed by XX user"), adding tracking tokens, and more. -The transformers' folder can be found at `follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms`. - -### Truncation -During this final step, FRS trims the candidate pool to a specified size. This process ensures that only the most relevant and engaging candidates are presented to users while maintaining an optimal user experience. - -By implementing these comprehensive steps and adapting to various use cases, the Follow Recommendations Service (FRS) effectively curates tailored suggestions for Twitter users, enhancing their overall experience and promoting meaningful connections within the platform. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/BUILD deleted file mode 100644 index d02506a85..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/BUILD +++ /dev/null @@ -1,18 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/guava", - "configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi", - "finagle/finagle-core/src/main", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "hermit/hermit-core/src/main/scala/com/twitter/hermit/model", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/recommendation", - "stitch/stitch-core", - ], - exports = [ - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/recommendation", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/CandidateSourceRegistry.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/CandidateSourceRegistry.scala deleted file mode 100644 index ee9cfbbe5..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/CandidateSourceRegistry.scala +++ /dev/null @@ -1,36 +0,0 @@ -package com.twitter.follow_recommendations.common.base - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.EnrichedCandidateSource.toEnriched -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier - -// a helper structure to register and select candidate sources based on identifiers -trait CandidateSourceRegistry[Target, Candidate] { - - val statsReceiver: StatsReceiver - - def sources: Set[CandidateSource[Target, Candidate]] - - final lazy val candidateSources: Map[ - CandidateSourceIdentifier, - CandidateSource[Target, Candidate] - ] = { - val map = sources.map { c => - c.identifier -> c.observe(statsReceiver) - }.toMap - - if (map.size != sources.size) { - throw new IllegalArgumentException("Duplicate Candidate Source Identifiers") - } - - map - } - - def select( - identifiers: Set[CandidateSourceIdentifier] - ): Set[CandidateSource[Target, Candidate]] = { - // fails loud if the candidate source is not registered - identifiers.map(candidateSources(_)) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/EnrichedCandidateSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/EnrichedCandidateSource.scala deleted file mode 100644 index 9d8507528..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/EnrichedCandidateSource.scala +++ /dev/null @@ -1,164 +0,0 @@ -package com.twitter.follow_recommendations.common.base - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.stitch.Stitch -import com.twitter.util.Duration -import com.twitter.util.TimeoutException -import scala.language.implicitConversions - -class EnrichedCandidateSource[Target, Candidate](original: CandidateSource[Target, Candidate]) { - - /** - * Gate the candidate source based on the Predicate of target. - * It returns results only if the predicate returns Valid. - * - * @param predicate - * @return - */ - def gate(predicate: Predicate[Target]): CandidateSource[Target, Candidate] = { - throw new UnsupportedOperationException() - } - - def observe(statsReceiver: StatsReceiver): CandidateSource[Target, Candidate] = { - val originalIdentifier = original.identifier - val stats = statsReceiver.scope(originalIdentifier.name) - new CandidateSource[Target, Candidate] { - val identifier = originalIdentifier - override def apply(target: Target): Stitch[Seq[Candidate]] = { - StatsUtil.profileStitchSeqResults[Candidate](original(target), stats) - } - } - } - - /** - * Map target type into new target type (1 to optional mapping) - */ - def stitchMapKey[Target2]( - targetMapper: Target2 => Stitch[Option[Target]] - ): CandidateSource[Target2, Candidate] = { - val targetsMapper: Target2 => Stitch[Seq[Target]] = { target => - targetMapper(target).map(_.toSeq) - } - stitchMapKeys(targetsMapper) - } - - /** - * Map target type into new target type (1 to many mapping) - */ - def stitchMapKeys[Target2]( - targetMapper: Target2 => Stitch[Seq[Target]] - ): CandidateSource[Target2, Candidate] = { - new CandidateSource[Target2, Candidate] { - val identifier = original.identifier - override def apply(target: Target2): Stitch[Seq[Candidate]] = { - for { - mappedTargets <- targetMapper(target) - results <- Stitch.traverse(mappedTargets)(original(_)) - } yield results.flatten - } - } - } - - /** - * Map target type into new target type (1 to many mapping) - */ - def mapKeys[Target2]( - targetMapper: Target2 => Seq[Target] - ): CandidateSource[Target2, Candidate] = { - val stitchMapper: Target2 => Stitch[Seq[Target]] = { target => - Stitch.value(targetMapper(target)) - } - stitchMapKeys(stitchMapper) - } - - /** - * Map candidate types to new type based on candidateMapper - */ - def mapValues[Candidate2]( - candidateMapper: Candidate => Stitch[Option[Candidate2]] - ): CandidateSource[Target, Candidate2] = { - - new CandidateSource[Target, Candidate2] { - val identifier = original.identifier - override def apply(target: Target): Stitch[Seq[Candidate2]] = { - original(target).flatMap { candidates => - val results = Stitch.traverse(candidates)(candidateMapper(_)) - results.map(_.flatten) - } - } - } - } - - /** - * Map candidate types to new type based on candidateMapper - */ - def mapValue[Candidate2]( - candidateMapper: Candidate => Candidate2 - ): CandidateSource[Target, Candidate2] = { - val stitchMapper: Candidate => Stitch[Option[Candidate2]] = { c => - Stitch.value(Some(candidateMapper(c))) - } - mapValues(stitchMapper) - } - - /** - * This method wraps the candidate source in a designated timeout so that a single candidate - * source does not result in a timeout for the entire flow - */ - def within( - candidateTimeout: Duration, - statsReceiver: StatsReceiver - ): CandidateSource[Target, Candidate] = { - val originalIdentifier = original.identifier - val timeoutCounter = - statsReceiver.counter(originalIdentifier.name, "timeout") - - new CandidateSource[Target, Candidate] { - val identifier = originalIdentifier - override def apply(target: Target): Stitch[Seq[Candidate]] = { - original - .apply(target) - .within(candidateTimeout)(com.twitter.finagle.util.DefaultTimer) - .rescue { - case _: TimeoutException => - timeoutCounter.incr() - Stitch.Nil - } - } - } - } - - def failOpenWithin( - candidateTimeout: Duration, - statsReceiver: StatsReceiver - ): CandidateSource[Target, Candidate] = { - val originalIdentifier = original.identifier - val timeoutCounter = - statsReceiver.counter(originalIdentifier.name, "timeout") - - new CandidateSource[Target, Candidate] { - val identifier = originalIdentifier - override def apply(target: Target): Stitch[Seq[Candidate]] = { - original - .apply(target) - .within(candidateTimeout)(com.twitter.finagle.util.DefaultTimer) - .handle { - case _: TimeoutException => - timeoutCounter.incr() - Seq.empty - case e: Exception => - statsReceiver - .scope("candidate_source_error").scope(originalIdentifier.name).counter( - e.getClass.getSimpleName).incr - Seq.empty - } - } - } - } -} - -object EnrichedCandidateSource { - implicit def toEnriched[K, V](original: CandidateSource[K, V]): EnrichedCandidateSource[K, V] = - new EnrichedCandidateSource(original) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/ParamPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/ParamPredicate.scala deleted file mode 100644 index f457527bc..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/ParamPredicate.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.twitter.follow_recommendations.common.base - -import com.twitter.follow_recommendations.common.models.FilterReason.ParamReason -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Param - -case class ParamPredicate[Request <: HasParams](param: Param[Boolean]) extends Predicate[Request] { - - def apply(request: Request): Stitch[PredicateResult] = { - if (request.params(param)) { - Stitch.value(PredicateResult.Valid) - } else { - Stitch.value(PredicateResult.Invalid(Set(ParamReason(param.statName)))) - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Predicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Predicate.scala deleted file mode 100644 index e5b40ed82..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Predicate.scala +++ /dev/null @@ -1,282 +0,0 @@ -package com.twitter.follow_recommendations.common.base - -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.FilterReason -import com.twitter.stitch.Arrow -import com.twitter.stitch.Stitch - -trait Predicate[-Q] { - - def apply(item: Q): Stitch[PredicateResult] - def arrow: Arrow[Q, PredicateResult] = Arrow.apply(apply) - - def map[K](mapper: K => Q): Predicate[K] = Predicate(arrow.contramap(mapper)) - - /** - * check the predicate results for a batch of items for convenience. - * - * mark it as final to avoid potential abuse usage - */ - final def batch(items: Seq[Q]): Stitch[Seq[PredicateResult]] = { - this.arrow.traverse(items) - } - - /** - * Syntax sugar for functions which take in 2 inputs as a tuple. - */ - def apply[Q1, Q2](item1: Q1, item2: Q2)(implicit ev: ((Q1, Q2)) => Q): Stitch[PredicateResult] = { - apply((item1, item2)) - } - - /** - * Runs the predicates in sequence. The returned predicate will return true iff both the predicates return true. - * ie. it is an AND operation - * - * We short-circuit the evaluation, ie we don't evaluate the 2nd predicate if the 1st is false - * - * @param p predicate to run in sequence - * - * @return a new predicate object that represents the logical AND of both predicates - */ - def andThen[Q1 <: Q](p: Predicate[Q1]): Predicate[Q1] = { - Predicate({ query: Q1 => - apply(query).flatMap { - case PredicateResult.Valid => p(query) - case PredicateResult.Invalid(reasons) => Stitch.value(PredicateResult.Invalid(reasons)) - } - }) - } - - /** - * Creates a predicate which runs the current & given predicate in sequence. - * The returned predicate will return true if either current or given predicate returns true. - * That is, given predicate will be only run if current predicate returns false. - * - * @param p predicate to run in sequence - * - * @return new predicate object that represents the logical OR of both predicates. - * if both are invalid, the reason would be the set of all invalid reasons. - */ - def or[Q1 <: Q](p: Predicate[Q1]): Predicate[Q1] = { - Predicate({ query: Q1 => - apply(query).flatMap { - case PredicateResult.Valid => Stitch.value(PredicateResult.Valid) - case PredicateResult.Invalid(reasons) => - p(query).flatMap { - case PredicateResult.Valid => Stitch.value(PredicateResult.Valid) - case PredicateResult.Invalid(newReasons) => - Stitch.value(PredicateResult.Invalid(reasons ++ newReasons)) - } - } - }) - } - - /* - * Runs the predicate only if the provided predicate is valid, otherwise returns valid. - * */ - def gate[Q1 <: Q](gatingPredicate: Predicate[Q1]): Predicate[Q1] = { - Predicate { query: Q1 => - gatingPredicate(query).flatMap { result => - if (result == PredicateResult.Valid) { - apply(query) - } else { - Stitch.value(PredicateResult.Valid) - } - } - } - } - - def observe(statsReceiver: StatsReceiver): Predicate[Q] = Predicate( - StatsUtil.profilePredicateResult(this.arrow, statsReceiver)) - - def convertToFailOpenWithResultType(resultType: PredicateResult): Predicate[Q] = { - Predicate { query: Q => - apply(query).handle { - case _: Exception => - resultType - } - - } - } - -} - -class TruePredicate[Q] extends Predicate[Q] { - override def apply(item: Q): Stitch[PredicateResult] = Predicate.AlwaysTrueStitch -} - -class FalsePredicate[Q](reason: FilterReason) extends Predicate[Q] { - val InvalidResult = Stitch.value(PredicateResult.Invalid(Set(reason))) - override def apply(item: Q): Stitch[PredicateResult] = InvalidResult -} - -object Predicate { - - val AlwaysTrueStitch = Stitch.value(PredicateResult.Valid) - - val NumBatchesStat = "num_batches_stats" - val NumBatchesCount = "num_batches" - - def apply[Q](func: Q => Stitch[PredicateResult]): Predicate[Q] = new Predicate[Q] { - override def apply(item: Q): Stitch[PredicateResult] = func(item) - - override val arrow: Arrow[Q, PredicateResult] = Arrow(func) - } - - def apply[Q](outerArrow: Arrow[Q, PredicateResult]): Predicate[Q] = new Predicate[Q] { - override def apply(item: Q): Stitch[PredicateResult] = arrow(item) - - override val arrow: Arrow[Q, PredicateResult] = outerArrow - } - - /** - * Given some items, this function - * 1. chunks them up in groups - * 2. lazily applies a predicate on each group - * 3. filters based on the predicate - * 4. takes first numToTake items. - * - * If numToTake is satisfied, then any later predicates are not called. - * - * @param items items of type Q - * @param predicate predicate that determines whether an item is acceptable - * @param batchSize batch size to call the predicate with - * @param numToTake max number of items to return - * @param stats stats receiver - * @tparam Q type of item - * - * @return a future of K items - */ - def batchFilterTake[Q]( - items: Seq[Q], - predicate: Predicate[Q], - batchSize: Int, - numToTake: Int, - stats: StatsReceiver - ): Stitch[Seq[Q]] = { - - def take( - input: Iterator[Stitch[Seq[Q]]], - prev: Seq[Q], - takeSize: Int, - numOfBatch: Int - ): Stitch[(Seq[Q], Int)] = { - if (input.hasNext) { - val currFut = input.next() - currFut.flatMap { curr => - val taken = curr.take(takeSize) - val combined = prev ++ taken - if (taken.size < takeSize) - take(input, combined, takeSize - taken.size, numOfBatch + 1) - else Stitch.value((combined, numOfBatch + 1)) - } - } else { - Stitch.value((prev, numOfBatch)) - } - } - - val batchedItems = items.view.grouped(batchSize) - val batchedFutures = batchedItems.map { batch => - Stitch.traverse(batch)(predicate.apply).map { conds => - (batch.zip(conds)).withFilter(_._2.value).map(_._1) - } - } - take(batchedFutures, Nil, numToTake, 0).map { - case (filtered: Seq[Q], numOfBatch: Int) => - stats.stat(NumBatchesStat).add(numOfBatch) - stats.counter(NumBatchesCount).incr(numOfBatch) - filtered - } - } - - /** - * filter a list of items based on the predicate - * - * @param items a list of items - * @param predicate predicate of the item - * @tparam Q item type - * @return the list of items that satisfy the predicate - */ - def filter[Q](items: Seq[Q], predicate: Predicate[Q]): Stitch[Seq[Q]] = { - predicate.batch(items).map { results => - items.zip(results).collect { - case (item, PredicateResult.Valid) => item - } - } - } - - /** - * filter a list of items based on the predicate given the target - * - * @param target target item - * @param items a list of items - * @param predicate predicate of the (target, item) pair - * @tparam Q item type - * @return the list of items that satisfy the predicate given the target - */ - def filter[T, Q](target: T, items: Seq[Q], predicate: Predicate[(T, Q)]): Stitch[Seq[Q]] = { - predicate.batch(items.map(i => (target, i))).map { results => - items.zip(results).collect { - case (item, PredicateResult.Valid) => item - } - } - } - - /** - * Returns a predicate, where an element is true iff it that element is true for all input predicates. - * ie. it is an AND operation - * - * This is done concurrently. - * - * @param predicates list of predicates - * @tparam Q Type parameter - * - * @return new predicate object that is the logical "and" of the input predicates - */ - def andConcurrently[Q](predicates: Seq[Predicate[Q]]): Predicate[Q] = { - Predicate { query: Q => - Stitch.traverse(predicates)(p => p(query)).map { predicateResults => - val allInvalid = predicateResults - .collect { - case PredicateResult.Invalid(reason) => - reason - } - if (allInvalid.isEmpty) { - PredicateResult.Valid - } else { - val allInvalidReasons = allInvalid.reduce(_ ++ _) - PredicateResult.Invalid(allInvalidReasons) - } - } - } - } -} - -/** - * applies the underlying predicate when the param is on. - */ -abstract class GatedPredicateBase[Q]( - underlyingPredicate: Predicate[Q], - stats: StatsReceiver = NullStatsReceiver) - extends Predicate[Q] { - def gate(item: Q): Boolean - - val underlyingPredicateTotal = stats.counter("underlying_total") - val underlyingPredicateValid = stats.counter("underlying_valid") - val underlyingPredicateInvalid = stats.counter("underlying_invalid") - val notGatedCounter = stats.counter("not_gated") - - val ValidStitch: Stitch[PredicateResult.Valid.type] = Stitch.value(PredicateResult.Valid) - - override def apply(item: Q): Stitch[PredicateResult] = { - if (gate(item)) { - underlyingPredicateTotal.incr() - underlyingPredicate(item) - } else { - notGatedCounter.incr() - ValidStitch - } - } - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/PredicateResult.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/PredicateResult.scala deleted file mode 100644 index 002e90275..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/PredicateResult.scala +++ /dev/null @@ -1,18 +0,0 @@ -package com.twitter.follow_recommendations.common.base - -import com.twitter.follow_recommendations.common.models.FilterReason - -sealed trait PredicateResult { - def value: Boolean -} - -object PredicateResult { - - case object Valid extends PredicateResult { - override val value = true - } - - case class Invalid(reasons: Set[FilterReason] = Set.empty[FilterReason]) extends PredicateResult { - override val value = false - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Ranker.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Ranker.scala deleted file mode 100644 index 27eb50457..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Ranker.scala +++ /dev/null @@ -1,90 +0,0 @@ -package com.twitter.follow_recommendations.common.base - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.stitch.Stitch -import com.twitter.util.Duration -import com.twitter.util.TimeoutException - -/** - * Ranker is a special kind of transform that would only change the order of a list of items. - * If a single item is given, it "may" attach additional scoring information to the item. - * - * @tparam Target target to recommend the candidates - * @tparam Candidate candidate type to rank - */ -trait Ranker[Target, Candidate] extends Transform[Target, Candidate] { ranker => - - def rank(target: Target, candidates: Seq[Candidate]): Stitch[Seq[Candidate]] - - override def transform(target: Target, candidates: Seq[Candidate]): Stitch[Seq[Candidate]] = { - rank(target, candidates) - } - - override def observe(statsReceiver: StatsReceiver): Ranker[Target, Candidate] = { - val originalRanker = this - new Ranker[Target, Candidate] { - override def rank(target: Target, items: Seq[Candidate]): Stitch[Seq[Candidate]] = { - statsReceiver.counter(Transform.InputCandidatesCount).incr(items.size) - statsReceiver.stat(Transform.InputCandidatesStat).add(items.size) - StatsUtil.profileStitchSeqResults(originalRanker.rank(target, items), statsReceiver) - } - } - } - - def reverse: Ranker[Target, Candidate] = new Ranker[Target, Candidate] { - def rank(target: Target, candidates: Seq[Candidate]): Stitch[Seq[Candidate]] = - ranker.rank(target, candidates).map(_.reverse) - } - - def andThen(other: Ranker[Target, Candidate]): Ranker[Target, Candidate] = { - val original = this - new Ranker[Target, Candidate] { - def rank(target: Target, candidates: Seq[Candidate]): Stitch[Seq[Candidate]] = { - original.rank(target, candidates).flatMap { results => other.rank(target, results) } - } - } - } - - /** - * This method wraps the Ranker in a designated timeout. - * If the ranker timeouts, it would return the original candidates directly, - * instead of failing the whole recommendation flow - */ - def within(timeout: Duration, statsReceiver: StatsReceiver): Ranker[Target, Candidate] = { - val timeoutCounter = statsReceiver.counter("timeout") - val original = this - new Ranker[Target, Candidate] { - override def rank(target: Target, candidates: Seq[Candidate]): Stitch[Seq[Candidate]] = { - original - .rank(target, candidates) - .within(timeout)(com.twitter.finagle.util.DefaultTimer) - .rescue { - case _: TimeoutException => - timeoutCounter.incr() - Stitch.value(candidates) - } - } - } - } -} - -object Ranker { - - def chain[Target, Candidate]( - transformer: Transform[Target, Candidate], - ranker: Ranker[Target, Candidate] - ): Ranker[Target, Candidate] = { - new Ranker[Target, Candidate] { - def rank(target: Target, candidates: Seq[Candidate]): Stitch[Seq[Candidate]] = { - transformer - .transform(target, candidates) - .flatMap { results => ranker.rank(target, results) } - } - } - } -} - -class IdentityRanker[Target, Candidate] extends Ranker[Target, Candidate] { - def rank(target: Target, candidates: Seq[Candidate]): Stitch[Seq[Candidate]] = - Stitch.value(candidates) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/RecommendationFlow.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/RecommendationFlow.scala deleted file mode 100644 index 6bddc9751..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/RecommendationFlow.scala +++ /dev/null @@ -1,250 +0,0 @@ -package com.twitter.follow_recommendations.common.base - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.UniversalNoun -import com.twitter.product_mixer.core.model.common.identifier.RecommendationPipelineIdentifier -import com.twitter.product_mixer.core.pipeline.recommendation.RecommendationPipelineResult -import com.twitter.product_mixer.core.quality_factor.QualityFactorObserver -import com.twitter.stitch.Stitch - -/** - * configs for results generated from the recommendation flow - * - * @param desiredCandidateCount num of desired candidates to return - * @param batchForCandidatesCheck batch size for candidates check - */ -case class RecommendationResultsConfig(desiredCandidateCount: Int, batchForCandidatesCheck: Int) - -trait BaseRecommendationFlow[Target, Candidate <: UniversalNoun[Long]] { - val identifier = RecommendationPipelineIdentifier("RecommendationFlow") - - def process( - pipelineRequest: Target - ): Stitch[RecommendationPipelineResult[Candidate, Seq[Candidate]]] - - def mapKey[Target2](fn: Target2 => Target): BaseRecommendationFlow[Target2, Candidate] = { - val original = this - new BaseRecommendationFlow[Target2, Candidate] { - override def process( - pipelineRequest: Target2 - ): Stitch[RecommendationPipelineResult[Candidate, Seq[Candidate]]] = - original.process(fn(pipelineRequest)) - } - } -} - -/** - * Defines a typical recommendation flow to fetch, filter, rank and transform candidates. - * - * 1. targetEligibility: determine the eligibility of target request - * 2. candidateSources: fetch candidates from candidate sources based on target type - * 3. preRankerCandidateFilter: light filtering of candidates - * 4. ranker: ranking of candidates (could be composed of multiple stages, light ranking, heavy ranking and etc) - * 5. postRankerTransform: deduping, grouping, rule based promotion / demotions and etc - * 6. validateCandidates: heavy filters to determine the eligibility of the candidates. - * will only be applied to candidates that we expect to return. - * 7. transformResults: transform the individual candidates into desired format (e.g. hydrate social proof) - * - * Note that the actual implementations may not need to implement all the steps if not needed - * (could just leave to IdentityRanker if ranking is not needed). - * - * Theoretically, the actual implementation could override the above flow to add - * more steps (e.g. add a transform step before ranking). - * But it is recommended to add the additional steps into this base flow if the step proves - * to have significant justification, or merge it into an existing step if it is a minor change. - * - * @tparam Target type of target request - * @tparam Candidate type of candidate to return - */ -trait RecommendationFlow[Target, Candidate <: UniversalNoun[Long]] - extends BaseRecommendationFlow[Target, Candidate] - with SideEffectsUtil[Target, Candidate] { - - /** - * optionally update or enrich the request before executing the flows - */ - protected def updateTarget(target: Target): Stitch[Target] = Stitch.value(target) - - /** - * check if the target is eligible for the flow - */ - protected def targetEligibility: Predicate[Target] - - /** - * define the candidate sources that should be used for the given target - */ - protected def candidateSources(target: Target): Seq[CandidateSource[Target, Candidate]] - - /** - * filter invalid candidates before the ranking phase. - */ - protected def preRankerCandidateFilter: Predicate[(Target, Candidate)] - - /** - * rank the candidates - */ - protected def selectRanker(target: Target): Ranker[Target, Candidate] - - /** - * transform the candidates after ranking (e.g. dedupping, grouping and etc) - */ - protected def postRankerTransform: Transform[Target, Candidate] - - /** - * filter invalid candidates before returning the results. - * - * Some heavy filters e.g. SGS filter could be applied in this step - */ - protected def validateCandidates: Predicate[(Target, Candidate)] - - /** - * transform the candidates into results and return - */ - protected def transformResults: Transform[Target, Candidate] - - /** - * configuration for recommendation results - */ - protected def resultsConfig(target: Target): RecommendationResultsConfig - - /** - * track the quality factor the recommendation pipeline - */ - protected def qualityFactorObserver: Option[QualityFactorObserver] = None - - def statsReceiver: StatsReceiver - - /** - * high level monitoring for the whole flow - * (make sure to add monitoring for each individual component by yourself) - * - * additional candidates: count, stats, non_empty_count - * target eligibility: latency, success, failures, request, count, valid_count, invalid_count, invalid_reasons - * candidate generation: latency, success, failures, request, count, non_empty_count, results_stat - * pre ranker filter: latency, success, failures, request, count, non_empty_count, results_stat - * ranker: latency, success, failures, request, count, non_empty_count, results_stat - * post ranker: latency, success, failures, request, count, non_empty_count, results_stat - * filter and take: latency, success, failures, request, count, non_empty_count, results_stat, batch count - * transform results: latency, success, failures, request, count, non_empty_count, results_stat - */ - import RecommendationFlow._ - lazy val additionalCandidatesStats = statsReceiver.scope(AdditionalCandidatesStats) - lazy val targetEligibilityStats = statsReceiver.scope(TargetEligibilityStats) - lazy val candidateGenerationStats = statsReceiver.scope(CandidateGenerationStats) - lazy val preRankerFilterStats = statsReceiver.scope(PreRankerFilterStats) - lazy val rankerStats = statsReceiver.scope(RankerStats) - lazy val postRankerTransformStats = statsReceiver.scope(PostRankerTransformStats) - lazy val filterAndTakeStats = statsReceiver.scope(FilterAndTakeStats) - lazy val transformResultsStats = statsReceiver.scope(TransformResultsStats) - - lazy val overallStats = statsReceiver.scope(OverallStats) - - import StatsUtil._ - - override def process( - pipelineRequest: Target - ): Stitch[RecommendationPipelineResult[Candidate, Seq[Candidate]]] = { - - observeStitchQualityFactor( - profileStitchSeqResults( - updateTarget(pipelineRequest).flatMap { target => - profilePredicateResult(targetEligibility(target), targetEligibilityStats).flatMap { - case PredicateResult.Valid => processValidTarget(target, Seq.empty) - case PredicateResult.Invalid(_) => Stitch.Nil - } - }, - overallStats - ).map { candidates => - RecommendationPipelineResult.empty.withResult(candidates) - }, - qualityFactorObserver, - overallStats - ) - } - - protected def processValidTarget( - target: Target, - additionalCandidates: Seq[Candidate] - ): Stitch[Seq[Candidate]] = { - - /** - * A basic recommendation flow looks like this: - * - * 1. fetch candidates from candidate sources - * 2. blend candidates with existing candidates - * 3. filter the candidates (light filters) before ranking - * 4. ranking - * 5. filter and truncate the candidates using postRankerCandidateFilter - * 6. transform the candidates based on product requirement - */ - val candidateSourcesToFetch = candidateSources(target) - for { - candidates <- profileStitchSeqResults( - Stitch.traverse(candidateSourcesToFetch)(_(target)).map(_.flatten), - candidateGenerationStats - ) - mergedCandidates = - profileSeqResults(additionalCandidates, additionalCandidatesStats) ++ - candidates - filteredCandidates <- profileStitchSeqResults( - Predicate.filter(target, mergedCandidates, preRankerCandidateFilter), - preRankerFilterStats - ) - rankedCandidates <- profileStitchSeqResults( - selectRanker(target).rank(target, filteredCandidates), - rankerStats - ) - transformed <- profileStitchSeqResults( - postRankerTransform.transform(target, rankedCandidates), - postRankerTransformStats - ) - truncated <- profileStitchSeqResults( - take(target, transformed, resultsConfig(target)), - filterAndTakeStats - ) - results <- profileStitchSeqResults( - transformResults.transform(target, truncated), - transformResultsStats - ) - _ <- applySideEffects( - target, - candidateSourcesToFetch, - candidates, - mergedCandidates, - filteredCandidates, - rankedCandidates, - transformed, - truncated, - results) - } yield results - } - - private[this] def take( - target: Target, - candidates: Seq[Candidate], - config: RecommendationResultsConfig - ): Stitch[Seq[Candidate]] = { - Predicate - .batchFilterTake( - candidates.map(c => (target, c)), - validateCandidates, - config.batchForCandidatesCheck, - config.desiredCandidateCount, - statsReceiver - ).map(_.map(_._2)) - } -} - -object RecommendationFlow { - - val AdditionalCandidatesStats = "additional_candidates" - val TargetEligibilityStats = "target_eligibility" - val CandidateGenerationStats = "candidate_generation" - val PreRankerFilterStats = "pre_ranker_filter" - val RankerStats = "ranker" - val PostRankerTransformStats = "post_ranker_transform" - val FilterAndTakeStats = "filter_and_take" - val TransformResultsStats = "transform_results" - val OverallStats = "overall" -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/SideEffectsUtil.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/SideEffectsUtil.scala deleted file mode 100644 index 2c922f580..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/SideEffectsUtil.scala +++ /dev/null @@ -1,24 +0,0 @@ -package com.twitter.follow_recommendations.common.base - -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.stitch.Stitch - -/** - * SideEffectsUtil applies side effects to the intermediate candidate results from a recommendation flow pipeline. - * - * @tparam Target target to recommend the candidates - * @tparam Candidate candidate type to rank - */ -trait SideEffectsUtil[Target, Candidate] { - def applySideEffects( - target: Target, - candidateSources: Seq[CandidateSource[Target, Candidate]], - candidatesFromCandidateSources: Seq[Candidate], - mergedCandidates: Seq[Candidate], - filteredCandidates: Seq[Candidate], - rankedCandidates: Seq[Candidate], - transformedCandidates: Seq[Candidate], - truncatedCandidates: Seq[Candidate], - results: Seq[Candidate] - ): Stitch[Unit] = Stitch.Unit -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/StatsUtil.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/StatsUtil.scala deleted file mode 100644 index eb99a909e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/StatsUtil.scala +++ /dev/null @@ -1,272 +0,0 @@ -package com.twitter.follow_recommendations.common.base -import com.twitter.finagle.stats.Stat -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.product_mixer.core.quality_factor.QualityFactorObserver -import com.twitter.stitch.Arrow -import com.twitter.stitch.Stitch -import com.twitter.util.Stopwatch -import java.util.concurrent.TimeUnit -import scala.util.control.NonFatal - -object StatsUtil { - val LatencyName = "latency_ms" - val RequestName = "requests" - val SuccessName = "success" - val FailureName = "failures" - val ResultsName = "results" - val ResultsStat = "results_stat" - val EmptyResultsName = "empty" - val NonEmptyResultsName = "non_empty" - val ValidCount = "valid" - val InvalidCount = "invalid" - val InvalidHasReasons = "has_reasons" - val Reasons = "reasons" - val QualityFactorStat = "quality_factor_stat" - val QualityFactorCounts = "quality_factor_counts" - - /** - * Helper function for timing a stitch, returning the original stitch. - */ - def profileStitch[T](stitch: Stitch[T], stat: StatsReceiver): Stitch[T] = { - - Stitch - .time(stitch) - .map { - case (response, stitchRunDuration) => - stat.counter(RequestName).incr() - stat.stat(LatencyName).add(stitchRunDuration.inMilliseconds) - response - .onSuccess { _ => stat.counter(SuccessName).incr() } - .onFailure { e => - stat.counter(FailureName).incr() - stat.scope(FailureName).counter(getCleanClassName(e)).incr() - } - } - .lowerFromTry - } - - /** - * Helper function for timing an arrow, returning the original arrow. - */ - def profileArrow[T, U](arrow: Arrow[T, U], stat: StatsReceiver): Arrow[T, U] = { - - Arrow - .time(arrow) - .map { - case (response, stitchRunDuration) => - stat.counter(RequestName).incr() - stat.stat(LatencyName).add(stitchRunDuration.inMilliseconds) - response - .onSuccess { _ => stat.counter(SuccessName).incr() } - .onFailure { e => - stat.counter(FailureName).incr() - stat.scope(FailureName).counter(getCleanClassName(e)).incr() - } - } - .lowerFromTry - } - - /** - * Helper function to count and track the distribution of results - */ - def profileResults[T](results: T, stat: StatsReceiver, size: T => Int): T = { - val numResults = size(results) - stat.counter(ResultsName).incr(numResults) - if (numResults == 0) { - stat.counter(EmptyResultsName).incr() - results - } else { - stat.stat(ResultsStat).add(numResults) - stat.counter(NonEmptyResultsName).incr() - results - } - } - - /** - * Helper function to count and track the distribution of a list of results - */ - def profileSeqResults[T](results: Seq[T], stat: StatsReceiver): Seq[T] = { - profileResults[Seq[T]](results, stat, _.size) - } - - /** - * Helper function for timing a stitch and count the number of results, returning the original stitch. - */ - def profileStitchResults[T](stitch: Stitch[T], stat: StatsReceiver, size: T => Int): Stitch[T] = { - profileStitch(stitch, stat).onSuccess { results => profileResults(results, stat, size) } - } - - /** - * Helper function for timing an arrow and count the number of results, returning the original arrow. - */ - def profileArrowResults[T, U]( - arrow: Arrow[T, U], - stat: StatsReceiver, - size: U => Int - ): Arrow[T, U] = { - profileArrow(arrow, stat).onSuccess { results => profileResults(results, stat, size) } - } - - /** - * Helper function for timing a stitch and count a seq of results, returning the original stitch. - */ - def profileStitchSeqResults[T](stitch: Stitch[Seq[T]], stat: StatsReceiver): Stitch[Seq[T]] = { - profileStitchResults[Seq[T]](stitch, stat, _.size) - } - - /** - * Helper function for timing a stitch and count optional results, returning the original stitch. - */ - def profileStitchOptionalResults[T]( - stitch: Stitch[Option[T]], - stat: StatsReceiver - ): Stitch[Option[T]] = { - profileStitchResults[Option[T]](stitch, stat, _.size) - } - - /** - * Helper function for timing a stitch and count a map of results, returning the original stitch. - */ - def profileStitchMapResults[K, V]( - stitch: Stitch[Map[K, V]], - stat: StatsReceiver - ): Stitch[Map[K, V]] = { - profileStitchResults[Map[K, V]](stitch, stat, _.size) - } - - def getCleanClassName(obj: Object): String = - obj.getClass.getSimpleName.stripSuffix("$") - - /** - * Helper function for timing a stitch and count a list of PredicateResult - */ - def profilePredicateResults( - predicateResult: Stitch[Seq[PredicateResult]], - statsReceiver: StatsReceiver - ): Stitch[Seq[PredicateResult]] = { - profileStitch[Seq[PredicateResult]]( - predicateResult, - statsReceiver - ).onSuccess { - _.map { - case PredicateResult.Valid => - statsReceiver.counter(ValidCount).incr() - case PredicateResult.Invalid(reasons) => - statsReceiver.counter(InvalidCount).incr() - reasons.map { filterReason => - statsReceiver.counter(InvalidHasReasons).incr() - statsReceiver.scope(Reasons).counter(filterReason.reason).incr() - } - } - } - } - - /** - * Helper function for timing a stitch and count individual PredicateResult - */ - def profilePredicateResult( - predicateResult: Stitch[PredicateResult], - statsReceiver: StatsReceiver - ): Stitch[PredicateResult] = { - profilePredicateResults( - predicateResult.map(Seq(_)), - statsReceiver - ).map(_.head) - } - - /** - * Helper function for timing an arrow and count a list of PredicateResult - */ - def profilePredicateResults[Q]( - predicateResult: Arrow[Q, Seq[PredicateResult]], - statsReceiver: StatsReceiver - ): Arrow[Q, Seq[PredicateResult]] = { - profileArrow[Q, Seq[PredicateResult]]( - predicateResult, - statsReceiver - ).onSuccess { - _.map { - case PredicateResult.Valid => - statsReceiver.counter(ValidCount).incr() - case PredicateResult.Invalid(reasons) => - statsReceiver.counter(InvalidCount).incr() - reasons.map { filterReason => - statsReceiver.counter(InvalidHasReasons).incr() - statsReceiver.scope(Reasons).counter(filterReason.reason).incr() - } - } - } - } - - /** - * Helper function for timing an arrow and count individual PredicateResult - */ - def profilePredicateResult[Q]( - predicateResult: Arrow[Q, PredicateResult], - statsReceiver: StatsReceiver - ): Arrow[Q, PredicateResult] = { - profilePredicateResults( - predicateResult.map(Seq(_)), - statsReceiver - ).map(_.head) - } - - /** - * Helper function for timing a stitch code block - */ - def profileStitchSeqResults[T]( - stats: StatsReceiver - )( - block: => Stitch[Seq[T]] - ): Stitch[Seq[T]] = { - stats.counter(RequestName).incr() - profileStitch(stats.stat(LatencyName), TimeUnit.MILLISECONDS) { - block onSuccess { r => - if (r.isEmpty) stats.counter(EmptyResultsName).incr() - stats.stat(ResultsStat).add(r.size) - } onFailure { e => - { - stats.counter(FailureName).incr() - stats.scope(FailureName).counter(e.getClass.getName).incr() - } - } - } - } - - /** - * Time a given asynchronous `f` using the given `unit`. - */ - def profileStitch[A](stat: Stat, unit: TimeUnit)(f: => Stitch[A]): Stitch[A] = { - val start = Stopwatch.timeNanos() - try { - f.respond { _ => stat.add(unit.convert(Stopwatch.timeNanos() - start, TimeUnit.NANOSECONDS)) } - } catch { - case NonFatal(e) => - stat.add(unit.convert(Stopwatch.timeNanos() - start, TimeUnit.NANOSECONDS)) - Stitch.exception(e) - } - } - - def observeStitchQualityFactor[T]( - stitch: Stitch[T], - qualityFactorObserverOption: Option[QualityFactorObserver], - statsReceiver: StatsReceiver - ): Stitch[T] = { - qualityFactorObserverOption - .map { observer => - Stitch - .time(stitch) - .map { - case (response, stitchRunDuration) => - observer(response, stitchRunDuration) - val qfVal = observer.qualityFactor.currentValue.floatValue() * 10000 - statsReceiver.counter(QualityFactorCounts).incr() - statsReceiver - .stat(QualityFactorStat) - .add(qfVal) - response - } - .lowerFromTry - }.getOrElse(stitch) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Transform.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Transform.scala deleted file mode 100644 index c870db2f6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base/Transform.scala +++ /dev/null @@ -1,85 +0,0 @@ -package com.twitter.follow_recommendations.common.base - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Param - -/** - * transform a or a list of candidate for target T - * - * @tparam T target type - * @tparam C candidate type - */ -trait Transform[-T, C] { - - // you need to implement at least one of the two methods here. - def transformItem(target: T, item: C): Stitch[C] = { - transform(target, Seq(item)).map(_.head) - } - - def transform(target: T, items: Seq[C]): Stitch[Seq[C]] - - def mapTarget[T2](mapper: T2 => T): Transform[T2, C] = { - val original = this - new Transform[T2, C] { - override def transformItem(target: T2, item: C): Stitch[C] = { - original.transformItem(mapper(target), item) - } - override def transform(target: T2, items: Seq[C]): Stitch[Seq[C]] = { - original.transform(mapper(target), items) - } - } - } - - /** - * sequential composition. we execute this' transform first, followed by the other's transform - */ - def andThen[T1 <: T](other: Transform[T1, C]): Transform[T1, C] = { - val original = this - new Transform[T1, C] { - override def transformItem(target: T1, item: C): Stitch[C] = - original.transformItem(target, item).flatMap(other.transformItem(target, _)) - override def transform(target: T1, items: Seq[C]): Stitch[Seq[C]] = - original.transform(target, items).flatMap(other.transform(target, _)) - } - } - - def observe(statsReceiver: StatsReceiver): Transform[T, C] = { - val originalTransform = this - new Transform[T, C] { - override def transform(target: T, items: Seq[C]): Stitch[Seq[C]] = { - statsReceiver.counter(Transform.InputCandidatesCount).incr(items.size) - statsReceiver.stat(Transform.InputCandidatesStat).add(items.size) - StatsUtil.profileStitchSeqResults(originalTransform.transform(target, items), statsReceiver) - } - - override def transformItem(target: T, item: C): Stitch[C] = { - statsReceiver.counter(Transform.InputCandidatesCount).incr() - StatsUtil.profileStitch(originalTransform.transformItem(target, item), statsReceiver) - } - } - } -} - -trait GatedTransform[T <: HasParams, C] extends Transform[T, C] { - def gated(param: Param[Boolean]): Transform[T, C] = { - val original = this - (target: T, items: Seq[C]) => { - if (target.params(param)) { - original.transform(target, items) - } else { - Stitch.value(items) - } - } - } -} - -object Transform { - val InputCandidatesCount = "input_candidates" - val InputCandidatesStat = "input_candidates_stat" -} - -class IdentityTransform[T, C] extends Transform[T, C] { - override def transform(target: T, items: Seq[C]): Stitch[Seq[C]] = Stitch.value(items) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/AddressBookParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/AddressBookParams.scala deleted file mode 100644 index f93a60d1b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/AddressBookParams.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.addressbook - -import com.twitter.timelines.configapi.FSParam - -object AddressBookParams { - // Used by display locations that want only to read from the ABV2 Client and ignore Manhattan - // Currently the only display location that does this is the ABUploadInjection DisplayLocation - object ReadFromABV2Only extends FSParam[Boolean]("addressbook_read_only_from_abv2", false) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/BUILD deleted file mode 100644 index ddbabad19..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/BUILD +++ /dev/null @@ -1,27 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders", - "src/thrift/com/twitter/hermit/candidate:hermit-candidate-scala", - "src/thrift/com/twitter/hermit/usercontacts:hermit-usercontacts-scala", - "strato/config/columns/onboarding/userrecs:userrecs-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ForwardEmailBookSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ForwardEmailBookSource.scala deleted file mode 100644 index d291459ce..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ForwardEmailBookSource.scala +++ /dev/null @@ -1,74 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.addressbook - -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.AddressBookParams.ReadFromABV2Only -import com.twitter.follow_recommendations.common.clients.addressbook.AddressbookClient -import com.twitter.follow_recommendations.common.clients.addressbook.models.EdgeType -import com.twitter.follow_recommendations.common.clients.addressbook.models.RecordIdentifier -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.utils.RescueWithStatsUtils.rescueWithStats -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.userrecs.ForwardEmailBookClientColumn -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ForwardEmailBookSource @Inject() ( - forwardEmailBookClientColumn: ForwardEmailBookClientColumn, - addressBookClient: AddressbookClient, - statsReceiver: StatsReceiver = NullStatsReceiver) - extends CandidateSource[HasParams with HasClientContext, CandidateUser] { - - override val identifier: CandidateSourceIdentifier = - ForwardEmailBookSource.Identifier - private val stats: StatsReceiver = statsReceiver.scope(this.getClass.getSimpleName) - - /** - * Generate a list of candidates for the target - */ - override def apply( - target: HasParams with HasClientContext - ): Stitch[Seq[CandidateUser]] = { - val candidateUsers: Stitch[Seq[Long]] = target.getOptionalUserId - .map { userId => - rescueWithStats( - addressBookClient.getUsers( - userId = userId, - identifiers = - Seq(RecordIdentifier(userId = Some(userId), email = None, phoneNumber = None)), - batchSize = AddressbookClient.AddressBook2BatchSize, - edgeType = ForwardEmailBookSource.DefaultEdgeType, - fetcherOption = - if (target.params.apply(ReadFromABV2Only)) None - else Some(forwardEmailBookClientColumn.fetcher), - queryOption = AddressbookClient - .createQueryOption( - edgeType = ForwardEmailBookSource.DefaultEdgeType, - isPhone = ForwardEmailBookSource.IsPhone) - ), - stats, - "AddressBookClient" - ) - }.getOrElse(Stitch.Nil) - - candidateUsers - .map( - _.take(ForwardEmailBookSource.NumEmailBookEntries) - .map(CandidateUser(_, score = Some(CandidateUser.DefaultCandidateScore)) - .withCandidateSource(identifier))) - } -} - -object ForwardEmailBookSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.ForwardEmailBook.toString) - val NumEmailBookEntries: Int = 1000 - val IsPhone = false - val DefaultEdgeType: EdgeType = EdgeType.Forward -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ForwardPhoneBookSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ForwardPhoneBookSource.scala deleted file mode 100644 index bb1f61f05..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ForwardPhoneBookSource.scala +++ /dev/null @@ -1,72 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.addressbook - -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.AddressBookParams.ReadFromABV2Only -import com.twitter.follow_recommendations.common.clients.addressbook.AddressbookClient -import com.twitter.follow_recommendations.common.clients.addressbook.models.EdgeType -import com.twitter.follow_recommendations.common.clients.addressbook.models.RecordIdentifier -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.utils.RescueWithStatsUtils.rescueWithStats -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.userrecs.ForwardPhoneContactsClientColumn -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ForwardPhoneBookSource @Inject() ( - forwardPhoneContactsClientColumn: ForwardPhoneContactsClientColumn, - addressBookClient: AddressbookClient, - statsReceiver: StatsReceiver = NullStatsReceiver) - extends CandidateSource[HasParams with HasClientContext, CandidateUser] { - - override val identifier: CandidateSourceIdentifier = - ForwardPhoneBookSource.Identifier - private val stats: StatsReceiver = statsReceiver.scope(this.getClass.getSimpleName) - - /** - * Generate a list of candidates for the target - */ - override def apply(target: HasParams with HasClientContext): Stitch[Seq[CandidateUser]] = { - val candidateUsers: Stitch[Seq[Long]] = target.getOptionalUserId - .map { userId => - rescueWithStats( - addressBookClient.getUsers( - userId, - identifiers = - Seq(RecordIdentifier(userId = Some(userId), email = None, phoneNumber = None)), - batchSize = AddressbookClient.AddressBook2BatchSize, - edgeType = ForwardPhoneBookSource.DefaultEdgeType, - fetcherOption = - if (target.params.apply(ReadFromABV2Only)) None - else Some(forwardPhoneContactsClientColumn.fetcher), - queryOption = AddressbookClient - .createQueryOption( - edgeType = ForwardPhoneBookSource.DefaultEdgeType, - isPhone = ForwardPhoneBookSource.IsPhone) - ), - stats, - "AddressBookClient" - ) - }.getOrElse(Stitch.Nil) - - candidateUsers - .map( - _.take(ForwardPhoneBookSource.NumPhoneBookEntries) - .map(CandidateUser(_, score = Some(CandidateUser.DefaultCandidateScore)) - .withCandidateSource(identifier))) - } -} - -object ForwardPhoneBookSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.ForwardPhoneBook.toString) - val NumPhoneBookEntries: Int = 1000 - val IsPhone = true - val DefaultEdgeType: EdgeType = EdgeType.Forward -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/README.md deleted file mode 100644 index 37b04a638..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Address Book Candidate Source -Provides the accounts of a given user's forward and reverse phone and email book contacts. -It is only available when the user has synced their address book with the service. - diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ReverseEmailBookSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ReverseEmailBookSource.scala deleted file mode 100644 index 6e89f8c24..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ReverseEmailBookSource.scala +++ /dev/null @@ -1,78 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.addressbook - -import com.twitter.cds.contact_consent_state.thriftscala.PurposeOfProcessing -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.clients.addressbook.AddressbookClient -import com.twitter.follow_recommendations.common.clients.addressbook.models.EdgeType -import com.twitter.follow_recommendations.common.clients.addressbook.models.RecordIdentifier -import com.twitter.follow_recommendations.common.clients.email_storage_service.EmailStorageServiceClient -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.utils.RescueWithStatsUtils.rescueOptionalWithStats -import com.twitter.follow_recommendations.common.utils.RescueWithStatsUtils.rescueWithStats -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.userrecs.ReverseEmailContactsClientColumn -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ReverseEmailBookSource @Inject() ( - reverseEmailContactsClientColumn: ReverseEmailContactsClientColumn, - essClient: EmailStorageServiceClient, - addressBookClient: AddressbookClient, - statsReceiver: StatsReceiver = NullStatsReceiver) - extends CandidateSource[HasParams with HasClientContext, CandidateUser] { - override val identifier: CandidateSourceIdentifier = ReverseEmailBookSource.Identifier - private val rescueStats = statsReceiver.scope("ReverseEmailBookSource") - - /** - * Generate a list of candidates for the target - */ - override def apply(target: HasParams with HasClientContext): Stitch[Seq[CandidateUser]] = { - val reverseCandidatesFromEmail = target.getOptionalUserId - .map { userId => - val verifiedEmailStitchOpt = - rescueOptionalWithStats( - essClient.getVerifiedEmail(userId, PurposeOfProcessing.ContentRecommendations), - rescueStats, - "getVerifiedEmail") - verifiedEmailStitchOpt.flatMap { emailOpt => - rescueWithStats( - addressBookClient.getUsers( - userId = userId, - identifiers = emailOpt - .map(email => - RecordIdentifier(userId = None, email = Some(email), phoneNumber = None)).toSeq, - batchSize = ReverseEmailBookSource.NumEmailBookEntries, - edgeType = ReverseEmailBookSource.DefaultEdgeType, - fetcherOption = - if (target.params(AddressBookParams.ReadFromABV2Only)) None - else Some(reverseEmailContactsClientColumn.fetcher) - ), - rescueStats, - "AddressBookClient" - ) - } - }.getOrElse(Stitch.Nil) - - reverseCandidatesFromEmail.map( - _.take(ReverseEmailBookSource.NumEmailBookEntries) - .map( - CandidateUser(_, score = Some(CandidateUser.DefaultCandidateScore)) - .withCandidateSource(identifier)) - ) - } -} - -object ReverseEmailBookSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.ReverseEmailBookIbis.toString) - val NumEmailBookEntries: Int = 500 - val IsPhone = false - val DefaultEdgeType: EdgeType = EdgeType.Reverse -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ReversePhoneBookSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ReversePhoneBookSource.scala deleted file mode 100644 index 4dbe5a617..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook/ReversePhoneBookSource.scala +++ /dev/null @@ -1,77 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.addressbook - -import com.twitter.cds.contact_consent_state.thriftscala.PurposeOfProcessing -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.clients.addressbook.AddressbookClient -import com.twitter.follow_recommendations.common.clients.addressbook.models.EdgeType -import com.twitter.follow_recommendations.common.clients.addressbook.models.RecordIdentifier -import com.twitter.follow_recommendations.common.clients.phone_storage_service.PhoneStorageServiceClient -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.utils.RescueWithStatsUtils.rescueWithStats -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.userrecs.ReversePhoneContactsClientColumn -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ReversePhoneBookSource @Inject() ( - reversePhoneContactsClientColumn: ReversePhoneContactsClientColumn, - pssClient: PhoneStorageServiceClient, - addressBookClient: AddressbookClient, - statsReceiver: StatsReceiver = NullStatsReceiver) - extends CandidateSource[HasParams with HasClientContext, CandidateUser] { - - override val identifier: CandidateSourceIdentifier = ReversePhoneBookSource.Identifier - private val stats: StatsReceiver = statsReceiver.scope(this.getClass.getSimpleName) - - /** - * Generate a list of candidates for the target - */ - override def apply(target: HasParams with HasClientContext): Stitch[Seq[CandidateUser]] = { - val reverseCandidatesFromPhones: Stitch[Seq[Long]] = target.getOptionalUserId - .map { userId => - pssClient - .getPhoneNumbers(userId, PurposeOfProcessing.ContentRecommendations) - .flatMap { phoneNumbers => - rescueWithStats( - addressBookClient.getUsers( - userId = userId, - identifiers = phoneNumbers.map(phoneNumber => - RecordIdentifier(userId = None, email = None, phoneNumber = Some(phoneNumber))), - batchSize = ReversePhoneBookSource.NumPhoneBookEntries, - edgeType = ReversePhoneBookSource.DefaultEdgeType, - fetcherOption = - if (target.params(AddressBookParams.ReadFromABV2Only)) None - else Some(reversePhoneContactsClientColumn.fetcher), - queryOption = AddressbookClient.createQueryOption( - edgeType = ReversePhoneBookSource.DefaultEdgeType, - isPhone = ReversePhoneBookSource.IsPhone) - ), - stats, - "AddressBookClient" - ) - } - }.getOrElse(Stitch.Nil) - - reverseCandidatesFromPhones.map( - _.take(ReversePhoneBookSource.NumPhoneBookEntries) - .map( - CandidateUser(_, score = Some(CandidateUser.DefaultCandidateScore)) - .withCandidateSource(identifier)) - ) - } -} - -object ReversePhoneBookSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.ReversePhoneBook.toString) - val NumPhoneBookEntries: Int = 500 - val IsPhone = true - val DefaultEdgeType: EdgeType = EdgeType.Reverse -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/BUILD deleted file mode 100644 index 275137bf9..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/BUILD +++ /dev/null @@ -1,23 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "content-recommender/thrift/src/main/thrift:thrift-scala", - "escherbird/src/scala/com/twitter/escherbird/util/stitchcache", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source", - "src/scala/com/twitter/onboarding/relevance/features/ymbii", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/CachedCandidateSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/CachedCandidateSource.scala deleted file mode 100644 index c0196c304..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/CachedCandidateSource.scala +++ /dev/null @@ -1,26 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base - -import com.twitter.escherbird.util.stitchcache.StitchCache -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import com.twitter.util.Duration - -class CachedCandidateSource[K <: Object, V <: Object]( - candidateSource: CandidateSource[K, V], - maxCacheSize: Int, - cacheTTL: Duration, - statsReceiver: StatsReceiver, - override val identifier: CandidateSourceIdentifier) - extends CandidateSource[K, V] { - - private val cache = StitchCache[K, Seq[V]]( - maxCacheSize = maxCacheSize, - ttl = cacheTTL, - statsReceiver = statsReceiver.scope(identifier.name, "cache"), - underlyingCall = (k: K) => candidateSource(k) - ) - - override def apply(target: K): Stitch[Seq[V]] = cache.readThrough(target) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/ExperimentalCandidateSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/ExperimentalCandidateSource.scala deleted file mode 100644 index 9e2b0e6e9..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/ExperimentalCandidateSource.scala +++ /dev/null @@ -1,66 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Param -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier - -/** - * A wrapper of CandidateSource to make it easier to do experimentation - * on new candidate generation algorithms - * - * @param baseSource base candidate source - * @param darkreadAlgorithmParam controls whether or not to darkread candidates (fetch them even if they will not be included) - * @param keepCandidatesParam controls whether or not to keep candidates from the base source - * @param resultCountThresholdParam controls how many results the source must return to bucket the user and return results (greater-than-or-equal-to) - * @tparam T request type. it must extend HasParams - * @tparam V value type - */ -class ExperimentalCandidateSource[T <: HasParams, V]( - baseSource: CandidateSource[T, V], - darkreadAlgorithmParam: Param[Boolean], - keepCandidatesParam: Param[Boolean], - resultCountThresholdParam: Param[Int], - baseStatsReceiver: StatsReceiver) - extends CandidateSource[T, V] { - - override val identifier: CandidateSourceIdentifier = baseSource.identifier - private[base] val statsReceiver = - baseStatsReceiver.scope(s"Experimental/${identifier.name}") - private[base] val requestsCounter = statsReceiver.counter("requests") - private[base] val resultCountGreaterThanThresholdCounter = - statsReceiver.counter("with_results_at_or_above_count_threshold") - private[base] val keepResultsCounter = statsReceiver.counter("keep_results") - private[base] val discardResultsCounter = statsReceiver.counter("discard_results") - - override def apply(request: T): Stitch[Seq[V]] = { - if (request.params(darkreadAlgorithmParam)) { - requestsCounter.incr() - fetchFromCandidateSourceAndProcessResults(request) - } else { - Stitch.Nil - } - } - - private def fetchFromCandidateSourceAndProcessResults(request: T): Stitch[Seq[V]] = { - baseSource(request).map { results => - if (results.length >= request.params(resultCountThresholdParam)) { - processResults(results, request.params(keepCandidatesParam)) - } else { - Nil - } - } - } - - private def processResults(results: Seq[V], keepResults: Boolean): Seq[V] = { - resultCountGreaterThanThresholdCounter.incr() - if (keepResults) { - keepResultsCounter.incr() - results - } else { - discardResultsCounter.incr() - Nil - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/RealGraphExpansionRepository.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/RealGraphExpansionRepository.scala deleted file mode 100644 index 8add11fa6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/RealGraphExpansionRepository.scala +++ /dev/null @@ -1,208 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base - -import com.twitter.conversions.DurationOps._ -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.finagle.util.DefaultTimer -import com.twitter.follow_recommendations.common.candidate_sources.base.RealGraphExpansionRepository.DefaultScore -import com.twitter.follow_recommendations.common.candidate_sources.base.RealGraphExpansionRepository.MaxNumIntermediateNodesToKeep -import com.twitter.follow_recommendations.common.candidate_sources.base.RealGraphExpansionRepository.FirstDegreeCandidatesTimeout -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models._ -import com.twitter.onboarding.relevance.features.ymbii.ExpansionCandidateScores -import com.twitter.onboarding.relevance.features.ymbii.RawYMBIICandidateFeatures -import com.twitter.onboarding.relevance.store.thriftscala.CandidatesFollowedV1 -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import com.twitter.strato.client.Fetcher -import com.twitter.util.Duration -import scala.collection.immutable -import scala.util.control.NonFatal - -private final case class InterestExpansionCandidate( - userID: Long, - score: Double, - features: RawYMBIICandidateFeatures) - -abstract class RealGraphExpansionRepository[Request]( - realgraphExpansionStore: Fetcher[ - Long, - Unit, - CandidatesFollowedV1 - ], - override val identifier: CandidateSourceIdentifier, - statsReceiver: StatsReceiver = NullStatsReceiver, - maxUnderlyingCandidatesToQuery: Int = 50, - maxCandidatesToReturn: Int = 40, - overrideUnderlyingTimeout: Option[Duration] = None, - appendSocialProof: Boolean = false) - extends CandidateSource[ - Request, - CandidateUser - ] { - - val underlyingCandidateSource: Seq[ - CandidateSource[ - Request, - CandidateUser - ] - ] - - private val stats = statsReceiver.scope(this.getClass.getSimpleName).scope(identifier.name) - private val underlyingCandidateSourceFailureStats = - stats.scope("underlying_candidate_source_failure") - - def apply( - request: Request, - ): Stitch[Seq[CandidateUser]] = { - - val candidatesFromUnderlyingSourcesStitch: Seq[Stitch[Seq[CandidateUser]]] = - underlyingCandidateSource.map { candidateSource => - candidateSource - .apply(request) - .within(overrideUnderlyingTimeout.getOrElse(FirstDegreeCandidatesTimeout))( - DefaultTimer - ) - .handle { - case NonFatal(e) => - underlyingCandidateSourceFailureStats - .counter(candidateSource.identifier.name, e.getClass.getSimpleName).incr() - Seq.empty - } - } - - for { - underlyingCandidatesFromEachAlgo <- Stitch.collect(candidatesFromUnderlyingSourcesStitch) - // The first algorithm in the list has the highest priority. Depending on if its not - // populated, fall back to other algorithms. Once a particular algorithm is chosen, only - // take the top few candidates from the underlying store for expansion. - underlyingCandidatesTuple = - underlyingCandidatesFromEachAlgo - .zip(underlyingCandidateSource) - .find(_._1.nonEmpty) - - underlyingAlgorithmUsed: Option[CandidateSourceIdentifier] = underlyingCandidatesTuple.map { - case (_, candidateSource) => candidateSource.identifier - } - - // Take maxUnderlyingCandidatesToQuery to query realgraphExpansionStore - underlyingCandidates = - underlyingCandidatesTuple - .map { - case (candidates, candidateSource) => - stats - .scope("underlyingAlgorithmUsedScope").counter( - candidateSource.identifier.name).incr() - candidates - } - .getOrElse(Seq.empty) - .sortBy(_.score.getOrElse(DefaultScore))(Ordering.Double.reverse) - .take(maxUnderlyingCandidatesToQuery) - - underlyingCandidateMap: Map[Long, Double] = underlyingCandidates.map { candidate => - (candidate.id, candidate.score.getOrElse(DefaultScore)) - }.toMap - - expansionCandidates <- - Stitch - .traverse(underlyingCandidateMap.keySet.toSeq) { candidateId => - Stitch.join( - Stitch.value(candidateId), - realgraphExpansionStore.fetch(candidateId).map(_.v)) - - }.map(_.toMap) - - rerankedCandidates: Seq[InterestExpansionCandidate] = - rerankCandidateExpansions(underlyingCandidateMap, expansionCandidates) - - rerankedCandidatesFiltered = rerankedCandidates.take(maxCandidatesToReturn) - - } yield { - rerankedCandidatesFiltered.map { candidate => - val socialProofReason = if (appendSocialProof) { - val socialProofIds = candidate.features.expansionCandidateScores - .map(_.intermediateCandidateId) - Some( - Reason(Some( - AccountProof(followProof = Some(FollowProof(socialProofIds, socialProofIds.size)))))) - } else { - None - } - CandidateUser( - id = candidate.userID, - score = Some(candidate.score), - reason = socialProofReason, - userCandidateSourceDetails = Some( - UserCandidateSourceDetails( - primaryCandidateSource = Some(identifier), - candidateSourceFeatures = Map(identifier -> Seq(candidate.features)) - )) - ).addAddressBookMetadataIfAvailable(underlyingAlgorithmUsed.toSeq) - } - } - } - - /** - * Expands underlying candidates, returning them in sorted order. - * - * @param underlyingCandidatesMap A map from underlying candidate id to score - * @param expansionCandidateMap A map from underlying candidate id to optional expansion candidates - * @return A sorted sequence of expansion candidates and associated scores - */ - private def rerankCandidateExpansions( - underlyingCandidatesMap: Map[Long, Double], - expansionCandidateMap: Map[Long, Option[CandidatesFollowedV1]] - ): Seq[InterestExpansionCandidate] = { - - // extract features - val candidates: Seq[(Long, ExpansionCandidateScores)] = for { - (underlyingCandidateId, underlyingCandidateScore) <- underlyingCandidatesMap.toSeq - expansionCandidates = - expansionCandidateMap - .get(underlyingCandidateId) - .flatten - .map(_.candidatesFollowed) - .getOrElse(Seq.empty) - expansionCandidate <- expansionCandidates - } yield expansionCandidate.candidateID -> ExpansionCandidateScores( - underlyingCandidateId, - Some(underlyingCandidateScore), - Some(expansionCandidate.score) - ) - - // merge intermediate nodes for the same candidate - val dedupedCandidates: Seq[(Long, Seq[ExpansionCandidateScores])] = - candidates.groupBy(_._1).mapValues(_.map(_._2).sortBy(_.intermediateCandidateId)).toSeq - - // score the candidate - val candidatesWithTotalScore: Seq[((Long, Seq[ExpansionCandidateScores]), Double)] = - dedupedCandidates.map { candidate: (Long, Seq[ExpansionCandidateScores]) => - ( - candidate, - candidate._2.map { ieScore: ExpansionCandidateScores => - ieScore.scoreFromUserToIntermediateCandidate.getOrElse(DefaultScore) * - ieScore.scoreFromIntermediateToExpansionCandidate.getOrElse(DefaultScore) - }.sum) - } - - // sort candidate by score - for { - ((candidate, edges), score) <- candidatesWithTotalScore.sortBy(_._2)(Ordering[Double].reverse) - } yield InterestExpansionCandidate( - candidate, - score, - RawYMBIICandidateFeatures( - edges.size, - edges.take(MaxNumIntermediateNodesToKeep).to[immutable.Seq]) - ) - } - -} - -object RealGraphExpansionRepository { - private val FirstDegreeCandidatesTimeout: Duration = 250.milliseconds - private val MaxNumIntermediateNodesToKeep = 20 - private val DefaultScore = 0.0d - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SimilarUserExpanderParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SimilarUserExpanderParams.scala deleted file mode 100644 index a4a0c9784..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SimilarUserExpanderParams.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam - -object SimilarUserExpanderParams { - - case object EnableNonDirectFollowExpansion - extends FSParam[Boolean]("similar_user_enable_non_direct_follow_expansion", true) - - case object EnableSimsExpandSeedAccountsSort - extends FSParam[Boolean]("similar_user_enable_sims_expander_seed_account_sort", false) - - case object DefaultExpansionInputCount - extends FSBoundedParam[Int]( - name = "similar_user_default_expansion_input_count", - default = Integer.MAX_VALUE, - min = 0, - max = Integer.MAX_VALUE) - - case object DefaultFinalCandidatesReturnedCount - extends FSBoundedParam[Int]( - name = "similar_user_default_final_candidates_returned_count", - default = Integer.MAX_VALUE, - min = 0, - max = Integer.MAX_VALUE) - - case object DefaultEnableImplicitEngagedExpansion - extends FSParam[Boolean]("similar_user_enable_implicit_engaged_expansion", true) - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SimilarUserExpanderRepository.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SimilarUserExpanderRepository.scala deleted file mode 100644 index 336902b34..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SimilarUserExpanderRepository.scala +++ /dev/null @@ -1,313 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base - -import com.twitter.follow_recommendations.common.candidate_sources.base.SimilarUserExpanderParams.DefaultEnableImplicitEngagedExpansion -import com.twitter.follow_recommendations.common.candidate_sources.base.SimilarUserExpanderParams.DefaultExpansionInputCount -import com.twitter.follow_recommendations.common.candidate_sources.base.SimilarUserExpanderParams.DefaultFinalCandidatesReturnedCount -import com.twitter.follow_recommendations.common.candidate_sources.base.SimilarUserExpanderParams.EnableNonDirectFollowExpansion -import com.twitter.follow_recommendations.common.candidate_sources.base.SimilarUserExpanderParams.EnableSimsExpandSeedAccountsSort -import com.twitter.follow_recommendations.common.candidate_sources.base.SimilarUserExpanderRepository.DefaultCandidateBuilder -import com.twitter.follow_recommendations.common.candidate_sources.base.SimilarUserExpanderRepository.DefaultScore -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.EngagementType -import com.twitter.follow_recommendations.common.models.FollowProof -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.follow_recommendations.common.models.SimilarToProof -import com.twitter.follow_recommendations.common.models.UserCandidateSourceDetails -import com.twitter.hermit.candidate.thriftscala.Candidates -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import com.twitter.strato.client.Fetcher -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Params - -case class SecondDegreeCandidate(userId: Long, score: Double, socialProof: Option[Seq[Long]]) - -abstract class SimilarUserExpanderRepository[-Request <: HasParams]( - override val identifier: CandidateSourceIdentifier, - similarToCandidatesFetcher: Fetcher[ - Long, - Unit, - Candidates - ], - expansionInputSizeParam: FSBoundedParam[Int] = DefaultExpansionInputCount, - candidatesReturnedSizeParam: FSBoundedParam[Int] = DefaultFinalCandidatesReturnedCount, - enableImplicitEngagedExpansion: FSParam[Boolean] = DefaultEnableImplicitEngagedExpansion, - thresholdToAvoidExpansion: Int = 30, - maxExpansionPerCandidate: Option[Int] = None, - includingOriginalCandidates: Boolean = false, - scorer: (Double, Double) => Double = SimilarUserExpanderRepository.DefaultScorer, - aggregator: (Seq[Double]) => Double = ScoreAggregator.Max, - candidateBuilder: (Long, CandidateSourceIdentifier, Double, CandidateUser) => CandidateUser = - DefaultCandidateBuilder) - extends TwoHopExpansionCandidateSource[ - Request, - CandidateUser, - SecondDegreeCandidate, - CandidateUser - ] { - - val originalCandidateSource: CandidateSource[Request, CandidateUser] - val backupOriginalCandidateSource: Option[CandidateSource[Request, CandidateUser]] = None - - override def firstDegreeNodes(request: Request): Stitch[Seq[CandidateUser]] = { - - val originalCandidatesStitch: Stitch[Seq[CandidateUser]] = - originalCandidateSource(request) - - val backupCandidatesStitch: Stitch[Seq[CandidateUser]] = - if (request.params(EnableNonDirectFollowExpansion)) { - backupOriginalCandidateSource.map(_.apply(request)).getOrElse(Stitch.Nil) - } else { - Stitch.Nil - } - - val firstDegreeCandidatesCombinedStitch: Stitch[Seq[CandidateUser]] = - Stitch - .join(originalCandidatesStitch, backupCandidatesStitch).map { - case (firstDegreeOrigCandidates, backupFirstDegreeCandidates) => - if (request.params(EnableSimsExpandSeedAccountsSort)) { - firstDegreeOrigCandidates ++ backupFirstDegreeCandidates sortBy { - -_.score.getOrElse(DefaultScore) - } - } else { - firstDegreeOrigCandidates ++ backupFirstDegreeCandidates - } - } - - val candidatesAfterImplicitEngagementsRemovalStitch: Stitch[Seq[CandidateUser]] = - getCandidatesAfterImplicitEngagementFiltering( - request.params, - firstDegreeCandidatesCombinedStitch) - - val firstDegreeCandidatesCombinedTrimmed = candidatesAfterImplicitEngagementsRemovalStitch.map { - candidates: Seq[CandidateUser] => - candidates.take(request.params(expansionInputSizeParam)) - } - - firstDegreeCandidatesCombinedTrimmed.map { firstDegreeResults: Seq[CandidateUser] => - if (firstDegreeResults.nonEmpty && firstDegreeResults.size < thresholdToAvoidExpansion) { - firstDegreeResults - .groupBy(_.id).mapValues( - _.maxBy(_.score) - ).values.toSeq - } else { - Nil - } - } - - } - - override def secondaryDegreeNodes( - request: Request, - firstDegreeCandidate: CandidateUser - ): Stitch[Seq[SecondDegreeCandidate]] = { - similarToCandidatesFetcher.fetch(firstDegreeCandidate.id).map(_.v).map { candidateListOption => - candidateListOption - .map { candidatesList => - candidatesList.candidates.map(candidate => - SecondDegreeCandidate(candidate.userId, candidate.score, candidate.socialProof)) - }.getOrElse(Nil) - } - - } - - override def aggregateAndScore( - req: Request, - firstDegreeToSecondDegreeNodesMap: Map[CandidateUser, Seq[SecondDegreeCandidate]] - ): Stitch[Seq[CandidateUser]] = { - - val similarExpanderResults = firstDegreeToSecondDegreeNodesMap.flatMap { - case (firstDegreeCandidate, seqOfSecondDegreeCandidates) => - val sourceScore = firstDegreeCandidate.score.getOrElse(DefaultScore) - val results: Seq[CandidateUser] = seqOfSecondDegreeCandidates.map { secondDegreeCandidate => - val score = scorer(sourceScore, secondDegreeCandidate.score) - candidateBuilder(secondDegreeCandidate.userId, identifier, score, firstDegreeCandidate) - } - maxExpansionPerCandidate match { - case None => results - case Some(limit) => results.sortBy(-_.score.getOrElse(DefaultScore)).take(limit) - } - }.toSeq - - val allCandidates = { - if (includingOriginalCandidates) - firstDegreeToSecondDegreeNodesMap.keySet.toSeq - else - Nil - } ++ similarExpanderResults - - val groupedCandidates: Seq[CandidateUser] = allCandidates - .groupBy(_.id) - .flatMap { - case (_, candidates) => - val finalScore = aggregator(candidates.map(_.score.getOrElse(DefaultScore))) - val candidateSourceDetailsCombined = aggregateCandidateSourceDetails(candidates) - val accountSocialProofcombined = aggregateAccountSocialProof(candidates) - - candidates.headOption.map( - _.copy( - score = Some(finalScore), - reason = accountSocialProofcombined, - userCandidateSourceDetails = candidateSourceDetailsCombined) - .withCandidateSource(identifier)) - } - .toSeq - - Stitch.value( - groupedCandidates - .sortBy { -_.score.getOrElse(DefaultScore) }.take(req.params(candidatesReturnedSizeParam)) - ) - } - - def aggregateCandidateSourceDetails( - candidates: Seq[CandidateUser] - ): Option[UserCandidateSourceDetails] = { - candidates - .map { candidate => - candidate.userCandidateSourceDetails.map(_.candidateSourceScores).getOrElse(Map.empty) - }.reduceLeftOption { (scoreMap1, scoreMap2) => - scoreMap1 ++ scoreMap2 - }.map { - UserCandidateSourceDetails(primaryCandidateSource = None, _) - } - - } - - def aggregateAccountSocialProof(candidates: Seq[CandidateUser]): Option[Reason] = { - candidates - .map { candidate => - ( - candidate.reason - .flatMap(_.accountProof.flatMap(_.similarToProof.map(_.similarTo))).getOrElse(Nil), - candidate.reason - .flatMap(_.accountProof.flatMap(_.followProof.map(_.followedBy))).getOrElse(Nil), - candidate.reason - .flatMap(_.accountProof.flatMap(_.followProof.map(_.numIds))).getOrElse(0) - ) - }.reduceLeftOption { (accountProofOne, accountProofTwo) => - ( - // merge similarToIds - accountProofOne._1 ++ accountProofTwo._1, - // merge followedByIds - accountProofOne._2 ++ accountProofTwo._2, - // add numIds - accountProofOne._3 + accountProofTwo._3) - }.map { proofs => - Reason(accountProof = Some( - AccountProof( - similarToProof = Some(SimilarToProof(proofs._1)), - followProof = if (proofs._2.nonEmpty) Some(FollowProof(proofs._2, proofs._3)) else None - ))) - } - } - - def getCandidatesAfterImplicitEngagementFiltering( - params: Params, - firstDegreeCandidatesStitch: Stitch[Seq[CandidateUser]] - ): Stitch[Seq[CandidateUser]] = { - - if (!params(enableImplicitEngagedExpansion)) { - - /** - * Remove candidates whose engagement types only contain implicit engagements - * (e.g. Profile View, Tweet Click) and only expand those candidates who contain explicit - * engagements. - */ - firstDegreeCandidatesStitch.map { candidates => - candidates.filter { cand => - cand.engagements.exists(engage => - engage == EngagementType.Like || engage == EngagementType.Retweet || engage == EngagementType.Mention) - } - } - } else { - firstDegreeCandidatesStitch - } - } - -} - -object SimilarUserExpanderRepository { - val DefaultScorer: (Double, Double) => Double = (sourceScore: Double, similarScore: Double) => - similarScore - val MultiplyScorer: (Double, Double) => Double = (sourceScore: Double, similarScore: Double) => - sourceScore * similarScore - val SourceScorer: (Double, Double) => Double = (sourceScore: Double, similarScore: Double) => - sourceScore - - val DefaultScore = 0.0d - - val DefaultCandidateBuilder: ( - Long, - CandidateSourceIdentifier, - Double, - CandidateUser - ) => CandidateUser = - ( - userId: Long, - _: CandidateSourceIdentifier, - score: Double, - candidate: CandidateUser - ) => { - val originalCandidateSourceDetails = - candidate.userCandidateSourceDetails.flatMap { candSourceDetails => - candSourceDetails.primaryCandidateSource.map { primaryCandidateSource => - UserCandidateSourceDetails( - primaryCandidateSource = None, - candidateSourceScores = Map(primaryCandidateSource -> candidate.score)) - } - } - CandidateUser( - id = userId, - score = Some(score), - userCandidateSourceDetails = originalCandidateSourceDetails, - reason = - Some(Reason(Some(AccountProof(similarToProof = Some(SimilarToProof(Seq(candidate.id))))))) - ) - } - - val FollowClusterCandidateBuilder: ( - Long, - CandidateSourceIdentifier, - Double, - CandidateUser - ) => CandidateUser = - (userId: Long, _: CandidateSourceIdentifier, score: Double, candidate: CandidateUser) => { - val originalCandidateSourceDetails = - candidate.userCandidateSourceDetails.flatMap { candSourceDetails => - candSourceDetails.primaryCandidateSource.map { primaryCandidateSource => - UserCandidateSourceDetails( - primaryCandidateSource = None, - candidateSourceScores = Map(primaryCandidateSource -> candidate.score)) - } - } - - val originalFollowCluster = candidate.reason - .flatMap(_.accountProof.flatMap(_.followProof.map(_.followedBy))) - - CandidateUser( - id = userId, - score = Some(score), - userCandidateSourceDetails = originalCandidateSourceDetails, - reason = Some( - Reason( - Some( - AccountProof( - similarToProof = Some(SimilarToProof(Seq(candidate.id))), - followProof = originalFollowCluster.map(follows => - FollowProof(follows, follows.size))))) - ) - ) - } -} - -object ScoreAggregator { - // aggregate the same candidates with same id by taking the one with largest score - val Max: Seq[Double] => Double = (candidateScores: Seq[Double]) => { candidateScores.max } - - // aggregate the same candidates with same id by taking the sum of the scores - val Sum: Seq[Double] => Double = (candidateScores: Seq[Double]) => { candidateScores.sum } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSource.scala deleted file mode 100644 index f650c9f6d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSource.scala +++ /dev/null @@ -1,86 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.transforms.modify_social_proof.ModifySocialProof -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.Duration - -abstract class SocialProofEnforcedCandidateSource( - candidateSource: CandidateSource[HasClientContext with HasParams, CandidateUser], - modifySocialProof: ModifySocialProof, - minNumSocialProofsRequired: Int, - override val identifier: CandidateSourceIdentifier, - baseStatsReceiver: StatsReceiver) - extends CandidateSource[HasClientContext with HasParams, CandidateUser] { - - val statsReceiver = baseStatsReceiver.scope(identifier.name) - - override def apply(target: HasClientContext with HasParams): Stitch[Seq[CandidateUser]] = { - val mustCallSgs: Boolean = target.params(SocialProofEnforcedCandidateSourceParams.MustCallSgs) - val callSgsCachedColumn: Boolean = - target.params(SocialProofEnforcedCandidateSourceParams.CallSgsCachedColumn) - val QueryIntersectionIdsNum: Int = - target.params(SocialProofEnforcedCandidateSourceParams.QueryIntersectionIdsNum) - val MaxNumCandidatesToAnnotate: Int = - target.params(SocialProofEnforcedCandidateSourceParams.MaxNumCandidatesToAnnotate) - val gfsIntersectionIdsNum: Int = - target.params(SocialProofEnforcedCandidateSourceParams.GfsIntersectionIdsNum) - val sgsIntersectionIdsNum: Int = - target.params(SocialProofEnforcedCandidateSourceParams.SgsIntersectionIdsNum) - val gfsLagDuration: Duration = - target.params(SocialProofEnforcedCandidateSourceParams.GfsLagDurationInDays) - - candidateSource(target) - .flatMap { candidates => - val candidatesWithoutEnoughSocialProof = candidates - .collect { - case candidate if !candidate.followedBy.exists(_.size >= minNumSocialProofsRequired) => - candidate - } - statsReceiver - .stat("candidates_with_no_social_proofs").add(candidatesWithoutEnoughSocialProof.size) - val candidatesToAnnotate = - candidatesWithoutEnoughSocialProof.take(MaxNumCandidatesToAnnotate) - statsReceiver.stat("candidates_to_annotate").add(candidatesToAnnotate.size) - - val annotatedCandidatesMapStitch = target.getOptionalUserId - .map { userId => - modifySocialProof - .hydrateSocialProof( - userId, - candidatesToAnnotate, - Some(QueryIntersectionIdsNum), - mustCallSgs, - callSgsCachedColumn, - gfsLagDuration = gfsLagDuration, - gfsIntersectionIds = gfsIntersectionIdsNum, - sgsIntersectionIds = sgsIntersectionIdsNum - ).map { annotatedCandidates => - annotatedCandidates - .map(annotatedCandidate => (annotatedCandidate.id, annotatedCandidate)).toMap - } - }.getOrElse(Stitch.value(Map.empty[Long, CandidateUser])) - - annotatedCandidatesMapStitch.map { annotatedCandidatesMap => - candidates - .flatMap { candidate => - if (candidate.followedBy.exists(_.size >= minNumSocialProofsRequired)) { - Some(candidate) - } else { - annotatedCandidatesMap.get(candidate.id).collect { - case annotatedCandidate - if annotatedCandidate.followedBy.exists( - _.size >= minNumSocialProofsRequired) => - annotatedCandidate - } - } - }.map(_.withCandidateSource(identifier)) - } - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSourceFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSourceFSConfig.scala deleted file mode 100644 index a74164f28..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSourceFSConfig.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.timelines.configapi.Param -import com.twitter.util.Duration -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class SocialProofEnforcedCandidateSourceFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = - Seq( - SocialProofEnforcedCandidateSourceParams.MustCallSgs, - SocialProofEnforcedCandidateSourceParams.CallSgsCachedColumn, - ) - override val intFSParams: Seq[FSBoundedParam[Int]] = - Seq( - SocialProofEnforcedCandidateSourceParams.QueryIntersectionIdsNum, - SocialProofEnforcedCandidateSourceParams.MaxNumCandidatesToAnnotate, - SocialProofEnforcedCandidateSourceParams.GfsIntersectionIdsNum, - SocialProofEnforcedCandidateSourceParams.SgsIntersectionIdsNum, - ) - - override val durationFSParams: Seq[FSBoundedParam[Duration] with HasDurationConversion] = Seq( - SocialProofEnforcedCandidateSourceParams.GfsLagDurationInDays - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSourceParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSourceParams.scala deleted file mode 100644 index 36e50e59f..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/SocialProofEnforcedCandidateSourceParams.scala +++ /dev/null @@ -1,56 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base - -import com.twitter.conversions.DurationOps._ -import com.twitter.timelines.configapi.DurationConversion -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.util.Duration - -object SocialProofEnforcedCandidateSourceParams { - case object MustCallSgs - extends FSParam[Boolean]("social_proof_enforced_candidate_source_must_call_sgs", true) - - case object CallSgsCachedColumn - extends FSParam[Boolean]( - "social_proof_enforced_candidate_source_call_sgs_cached_column", - false) - - case object QueryIntersectionIdsNum - extends FSBoundedParam[Int]( - name = "social_proof_enforced_candidate_source_query_intersection_ids_num", - default = 3, - min = 0, - max = Integer.MAX_VALUE) - - case object MaxNumCandidatesToAnnotate - extends FSBoundedParam[Int]( - name = "social_proof_enforced_candidate_source_max_num_candidates_to_annotate", - default = 50, - min = 0, - max = Integer.MAX_VALUE) - - case object GfsIntersectionIdsNum - extends FSBoundedParam[Int]( - name = "social_proof_enforced_candidate_source_gfs_intersection_ids_num", - default = 3, - min = 0, - max = Integer.MAX_VALUE) - - case object SgsIntersectionIdsNum - extends FSBoundedParam[Int]( - name = "social_proof_enforced_candidate_source_sgs_intersection_ids_num", - default = 10, - min = 0, - max = Integer.MAX_VALUE) - - case object GfsLagDurationInDays - extends FSBoundedParam[Duration]( - name = "social_proof_enforced_candidate_source_gfs_lag_duration_in_days", - default = 14.days, - min = 1.days, - max = 60.days) - with HasDurationConversion { - override val durationConversion: DurationConversion = DurationConversion.FromDays - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/StratoFetcherSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/StratoFetcherSource.scala deleted file mode 100644 index ea6fa57fa..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/StratoFetcherSource.scala +++ /dev/null @@ -1,27 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base - -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import com.twitter.strato.client.Fetcher - -abstract class StratoFetcherSource[K, U, V]( - fetcher: Fetcher[K, U, V], - view: U, - override val identifier: CandidateSourceIdentifier) - extends CandidateSource[K, CandidateUser] { - - def map(user: K, v: V): Seq[CandidateUser] - - override def apply(target: K): Stitch[Seq[CandidateUser]] = { - fetcher - .fetch(target, view) - .map { result => - result.v - .map { candidates => map(target, candidates) } - .getOrElse(Nil) - .map(_.withCandidateSource(identifier)) - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/StratoFetcherWithUnitViewSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/StratoFetcherWithUnitViewSource.scala deleted file mode 100644 index 1f1572ee6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/StratoFetcherWithUnitViewSource.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base - -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.client.Fetcher - -abstract class StratoFetcherWithUnitViewSource[K, V]( - fetcher: Fetcher[K, Unit, V], - override val identifier: CandidateSourceIdentifier) - extends StratoFetcherSource[K, Unit, V](fetcher, Unit, identifier) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/TweetAuthorsCandidateSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/TweetAuthorsCandidateSource.scala deleted file mode 100644 index 541b24837..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/TweetAuthorsCandidateSource.scala +++ /dev/null @@ -1,71 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base - -import com.twitter.follow_recommendations.common.models.TweetCandidate -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.stitch.Stitch - -/** - * base trait for tweet authors based algorithms, e.g. topical tweet authors, twistly, ... - * - * @tparam Target target type - * @tparam Candidate output candidate types - */ -trait TweetAuthorsCandidateSource[-Target, +Candidate] extends CandidateSource[Target, Candidate] { - - /** - * fetch Tweet candidates - */ - def getTweetCandidates(target: Target): Stitch[Seq[TweetCandidate]] - - /** - * fetch authorId - */ - def getTweetAuthorId(tweetCandidate: TweetCandidate): Stitch[Option[Long]] - - /** - * wrap candidate ID and TweetAuthorProof in Candidate - */ - def toCandidate(authorId: Long, tweetIds: Seq[Long], score: Option[Double]): Candidate - - /** - * aggregate scores, default to the first score - */ - def aggregator(scores: Seq[Double]): Double = - scores.headOption.getOrElse(TweetAuthorsCandidateSource.DefaultScore) - - /** - * aggregation method for a group of tweet candidates - */ - def aggregateAndScore( - target: Target, - tweetCandidates: Seq[TweetCandidate] - ): Seq[Candidate] - - /** - * generate a list of candidates for the target - */ - def build( - target: Target - ): Stitch[Seq[Candidate]] = { - // Fetch Tweet candidates and hydrate author IDs - val tweetCandidatesStitch = for { - tweetCandidates <- getTweetCandidates(target) - authorIds <- Stitch.collect(tweetCandidates.map(getTweetAuthorId(_))) - } yield { - for { - (authorIdOpt, tweetCandidate) <- authorIds.zip(tweetCandidates) - authorId <- authorIdOpt - } yield tweetCandidate.copy(authorId = authorId) - } - - // Aggregate and score, convert to candidate - tweetCandidatesStitch.map(aggregateAndScore(target, _)) - } - - def apply(target: Target): Stitch[Seq[Candidate]] = - build(target) -} - -object TweetAuthorsCandidateSource { - final val DefaultScore: Double = 0.0 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/TwoHopExpansionCandidateSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/TwoHopExpansionCandidateSource.scala deleted file mode 100644 index 40c699d22..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base/TwoHopExpansionCandidateSource.scala +++ /dev/null @@ -1,46 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.base - -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.stitch.Stitch - -/** - * base trait for two-hop expansion based algorithms, e.g. online_stp, phonebook_prediction, - * recent following sims, recent engagement sims, ... - * - * @tparam Target target type - * @tparam FirstDegree type of first degree nodes - * @tparam SecondaryDegree type of secondary degree nodes - * @tparam Candidate output candidate types - */ -trait TwoHopExpansionCandidateSource[-Target, FirstDegree, SecondaryDegree, +Candidate] - extends CandidateSource[Target, Candidate] { - - /** - * fetch first degree nodes given request - */ - def firstDegreeNodes(req: Target): Stitch[Seq[FirstDegree]] - - /** - * fetch secondary degree nodes given request and first degree nodes - */ - def secondaryDegreeNodes(req: Target, node: FirstDegree): Stitch[Seq[SecondaryDegree]] - - /** - * aggregate and score the candidates to generate final results - */ - def aggregateAndScore( - req: Target, - firstDegreeToSecondDegreeNodesMap: Map[FirstDegree, Seq[SecondaryDegree]] - ): Stitch[Seq[Candidate]] - - /** - * Generate a list of candidates for the target - */ - def apply(target: Target): Stitch[Seq[Candidate]] = { - for { - firstDegreeNodes <- firstDegreeNodes(target) - secondaryDegreeNodes <- Stitch.traverse(firstDegreeNodes)(secondaryDegreeNodes(target, _)) - aggregated <- aggregateAndScore(target, firstDegreeNodes.zip(secondaryDegreeNodes).toMap) - } yield aggregated - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/BUILD deleted file mode 100644 index de3bc0a6b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/BUILD +++ /dev/null @@ -1,22 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - strict_deps = True, - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "escherbird/src/scala/com/twitter/escherbird/util/stitchcache", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "hermit/hermit-core/src/main/scala/com/twitter/hermit/model", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source", - "src/thrift/com/twitter/onboarding/relevance/crowd_search_accounts:crowd_search_accounts-scala", - "strato/config/columns/onboarding/userrecs:userrecs-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-core/src/main/scala/com/twitter/conversions", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsFSConfig.scala deleted file mode 100644 index 520983b60..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsFSConfig.scala +++ /dev/null @@ -1,18 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.Param -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class CrowdSearchAccountsFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = Seq( - CrowdSearchAccountsParams.CandidateSourceEnabled, - ) - override val doubleFSParams: Seq[FSBoundedParam[Double]] = Seq( - CrowdSearchAccountsParams.CandidateSourceWeight, - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsParams.scala deleted file mode 100644 index a167b7768..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsParams.scala +++ /dev/null @@ -1,32 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSEnumSeqParam -import com.twitter.timelines.configapi.FSParam - -object CrowdSearchAccountsParams { - // whether or not to fetch CrowdSearchAccounts candidate sources - case object CandidateSourceEnabled - extends FSParam[Boolean]("crowd_search_accounts_candidate_source_enabled", false) - - /** - * Contains the logic key for account filtering and ranking. Currently we have 3 main logic keys - * - new_daily: filtering top searched accounts with max daily searches based on new users - * - new_weekly: filtering top searched accounts with max weekly searches based on new users - * - daily: filtering top searched accounts with max daily searches - * - weekly: filtering top searched accounts with max weekly searches - * Mapping of the Logic Id to Logic key is done via @enum AccountsFilteringAndRankingLogic - */ - case object AccountsFilteringAndRankingLogics - extends FSEnumSeqParam[AccountsFilteringAndRankingLogicId.type]( - name = "crowd_search_accounts_filtering_and_ranking_logic_ids", - default = Seq(AccountsFilteringAndRankingLogicId.SearchesWeekly), - enum = AccountsFilteringAndRankingLogicId) - - case object CandidateSourceWeight - extends FSBoundedParam[Double]( - "crowd_search_accounts_candidate_source_weight", - default = 1200, - min = 0.001, - max = 2000) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsSource.scala deleted file mode 100644 index 6d3e903a1..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/CrowdSearchAccountsSource.scala +++ /dev/null @@ -1,111 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts - -import com.twitter.escherbird.util.stitchcache.StitchCache -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts.CrowdSearchAccountsParams.AccountsFilteringAndRankingLogics -import com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts.CrowdSearchAccountsParams.CandidateSourceEnabled -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasGeohashAndCountryCode -import com.twitter.hermit.model.Algorithm -import com.twitter.onboarding.relevance.crowd_search_accounts.thriftscala.CrowdSearchAccounts -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.userrecs.CrowdSearchAccountsClientColumn -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.Duration -import com.twitter.util.logging.Logging -import javax.inject.Inject -import javax.inject.Singleton - -object AccountsFilteringAndRankingLogicId extends Enumeration { - type AccountsFilteringAndRankingLogicId = Value - - val NewSearchesDaily: AccountsFilteringAndRankingLogicId = Value("new_searches_daily") - val NewSearchesWeekly: AccountsFilteringAndRankingLogicId = Value("new_searches_weekly") - val SearchesDaily: AccountsFilteringAndRankingLogicId = Value("searches_daily") - val SearchesWeekly: AccountsFilteringAndRankingLogicId = Value("searches_weekly") -} - -object CrowdSearchAccountsSource { - val MaxCacheSize = 500 - val CacheTTL: Duration = Duration.fromHours(24) - - type Target = HasParams with HasClientContext with HasGeohashAndCountryCode - - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.CrowdSearchAccounts.toString) -} - -@Singleton -class CrowdSearchAccountsSource @Inject() ( - crowdSearchAccountsClientColumn: CrowdSearchAccountsClientColumn, - statsReceiver: StatsReceiver, -) extends CandidateSource[CrowdSearchAccountsSource.Target, CandidateUser] - with Logging { - - /** @see [[CandidateSourceIdentifier]] */ - override val identifier: CandidateSourceIdentifier = - CrowdSearchAccountsSource.Identifier - - private val stats = statsReceiver.scope(identifier.name) - private val requestsStats = stats.counter("requests") - private val noCountryCodeStats = stats.counter("no_country_code") - private val successStats = stats.counter("success") - private val errorStats = stats.counter("error") - - private val cache = StitchCache[String, Option[CrowdSearchAccounts]]( - maxCacheSize = CrowdSearchAccountsSource.MaxCacheSize, - ttl = CrowdSearchAccountsSource.CacheTTL, - statsReceiver = statsReceiver.scope(identifier.name, "cache"), - underlyingCall = (k: String) => { - crowdSearchAccountsClientColumn.fetcher - .fetch(k) - .map { result => result.v } - } - ) - - /** returns a Seq of ''potential'' content */ - override def apply( - target: CrowdSearchAccountsSource.Target - ): Stitch[Seq[CandidateUser]] = { - if (!target.params(CandidateSourceEnabled)) { - return Stitch.value(Seq[CandidateUser]()) - } - requestsStats.incr() - target.getCountryCode - .orElse(target.geohashAndCountryCode.flatMap(_.countryCode)).map { countryCode => - Stitch - .collect(target - .params(AccountsFilteringAndRankingLogics).map(logic => - cache.readThrough(countryCode.toUpperCase() + "-" + logic))) - .onSuccess(_ => { - successStats.incr() - }) - .onFailure(t => { - debug("candidate source failed identifier = %s".format(identifier), t) - errorStats.incr() - }) - .map(transformCrowdSearchAccountsToCandidateSource) - }.getOrElse { - noCountryCodeStats.incr() - Stitch.value(Seq[CandidateUser]()) - } - } - - private def transformCrowdSearchAccountsToCandidateSource( - crowdSearchAccounts: Seq[Option[CrowdSearchAccounts]] - ): Seq[CandidateUser] = { - crowdSearchAccounts - .flatMap(opt => - opt - .map(accounts => - accounts.accounts.map(account => - CandidateUser( - id = account.accountId, - score = Some(account.searchActivityScore), - ).withCandidateSource(identifier))) - .getOrElse(Seq[CandidateUser]())) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/README.md deleted file mode 100644 index 043279b44..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Crowd Search Candidate Source -Provides the most searched accounts within a specific country over the past 1 and 7 days. -* When we refer to "most searched accounts", we are referring to accounts that have been clicked on the most frequently by users after they see search results in both the typeahead and search results page. -* The results returned by the service have undergone health filters. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/BUILD deleted file mode 100644 index 2493a4a93..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/BUILD +++ /dev/null @@ -1,23 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "src/thrift/com/twitter/hermit/pop_geo:hermit-pop-geo-scala", - "strato/config/columns/onboarding/userrecs:userrecs-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/BasePopGeoHashSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/BasePopGeoHashSource.scala deleted file mode 100644 index 862046bbc..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/BasePopGeoHashSource.scala +++ /dev/null @@ -1,74 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.geo - -import com.google.inject.Singleton -import com.twitter.finagle.stats.Counter -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasGeohashAndCountryCode -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject - -@Singleton -class BasePopGeohashSource @Inject() ( - popGeoSource: CandidateSource[String, CandidateUser], - statsReceiver: StatsReceiver) - extends CandidateSource[ - HasParams with HasClientContext with HasGeohashAndCountryCode, - CandidateUser - ] - with BasePopGeohashSourceConfig { - - val stats: StatsReceiver = statsReceiver - - // counter to check if we found a geohash value in the request - val foundGeohashCounter: Counter = stats.counter("found_geohash_value") - // counter to check if we are missing a geohash value in the request - val missingGeohashCounter: Counter = stats.counter("missing_geohash_value") - - /** @see [[CandidateSourceIdentifier]] */ - override val identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - "BasePopGeohashSource") - - override def apply( - target: HasParams with HasClientContext with HasGeohashAndCountryCode - ): Stitch[Seq[CandidateUser]] = { - if (!candidateSourceEnabled(target)) { - return Stitch.Nil - } - target.geohashAndCountryCode - .flatMap(_.geohash).map { geohash => - foundGeohashCounter.incr() - val keys = (minGeohashLength(target) to math.min(maxGeohashLength(target), geohash.length)) - .map("geohash_" + geohash.take(_)).reverse - if (returnResultFromAllPrecision(target)) { - Stitch - .collect(keys.map(popGeoSource.apply)).map( - _.flatten.map(_.withCandidateSource(identifier)) - ) - } else { - Stitch - .collect(keys.map(popGeoSource.apply)).map( - _.find(_.nonEmpty) - .getOrElse(Nil) - .take(maxResults(target)).map(_.withCandidateSource(identifier)) - ) - } - }.getOrElse { - missingGeohashCounter.incr() - Stitch.Nil - } - } -} - -trait BasePopGeohashSourceConfig { - type Target = HasParams with HasClientContext - def maxResults(target: Target): Int = 200 - def minGeohashLength(target: Target): Int = 2 - def maxGeohashLength(target: Target): Int = 4 - def returnResultFromAllPrecision(target: Target): Boolean = false - def candidateSourceEnabled(target: Target): Boolean = false -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopCountryBackFillSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopCountryBackFillSource.scala deleted file mode 100644 index e2a5f9baf..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopCountryBackFillSource.scala +++ /dev/null @@ -1,33 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.geo - -import com.google.inject.Singleton -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject - -@Singleton -class PopCountryBackFillSource @Inject() (popGeoSource: PopGeoSource) - extends CandidateSource[HasClientContext with HasParams, CandidateUser] { - - override val identifier: CandidateSourceIdentifier = PopCountryBackFillSource.Identifier - - override def apply(target: HasClientContext with HasParams): Stitch[Seq[CandidateUser]] = { - target.getOptionalUserId - .map(_ => - popGeoSource(PopCountryBackFillSource.DefaultKey) - .map(_.take(PopCountryBackFillSource.MaxResults).map(_.withCandidateSource(identifier)))) - .getOrElse(Stitch.Nil) - } -} - -object PopCountryBackFillSource { - val Identifier: CandidateSourceIdentifier = - CandidateSourceIdentifier(Algorithm.PopCountryBackFill.toString) - val MaxResults = 40 - val DefaultKey = "country_US" -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopCountrySource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopCountrySource.scala deleted file mode 100644 index de6377df6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopCountrySource.scala +++ /dev/null @@ -1,63 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.geo - -import com.google.inject.Singleton -import com.twitter.core_workflows.user_model.thriftscala.UserState -import com.twitter.finagle.stats.Counter -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasGeohashAndCountryCode -import com.twitter.follow_recommendations.common.models.HasUserState -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject - -@Singleton -class PopCountrySource @Inject() ( - popGeoSource: PopGeoSource, - statsReceiver: StatsReceiver) - extends CandidateSource[ - HasClientContext with HasParams with HasUserState with HasGeohashAndCountryCode, - CandidateUser - ] { - - override val identifier: CandidateSourceIdentifier = PopCountrySource.Identifier - val stats: StatsReceiver = statsReceiver.scope("PopCountrySource") - - // counter to check if we found a country code value in the request - val foundCountryCodeCounter: Counter = stats.counter("found_country_code_value") - // counter to check if we are missing a country code value in the request - val missingCountryCodeCounter: Counter = stats.counter("missing_country_code_value") - - override def apply( - target: HasClientContext with HasParams with HasUserState with HasGeohashAndCountryCode - ): Stitch[Seq[CandidateUser]] = { - target.geohashAndCountryCode - .flatMap(_.countryCode).map { countryCode => - foundCountryCodeCounter.incr() - if (target.userState.exists(PopCountrySource.BlacklistedTargetUserStates.contains)) { - Stitch.Nil - } else { - popGeoSource("country_" + countryCode) - .map(_.take(PopCountrySource.MaxResults).map(_.withCandidateSource(identifier))) - } - }.getOrElse { - missingCountryCodeCounter.incr() - Stitch.Nil - } - } -} - -object PopCountrySource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.PopCountry.toString) - val MaxResults = 40 - val BlacklistedTargetUserStates: Set[UserState] = Set( - UserState.HeavyTweeter, - UserState.HeavyNonTweeter, - UserState.MediumTweeter, - UserState.MediumNonTweeter) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSource.scala deleted file mode 100644 index ad473a282..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSource.scala +++ /dev/null @@ -1,99 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.geo -import com.google.inject.Singleton -import com.twitter.escherbird.util.stitchcache.StitchCache -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.PopularInGeoProof -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.hermit.model.Algorithm -import com.twitter.hermit.pop_geo.thriftscala.PopUsersInPlace -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.userrecs.UniquePopQualityFollowUsersInPlaceClientColumn -import com.twitter.util.Duration -import javax.inject.Inject - -@Singleton -class PopGeohashQualityFollowSource @Inject() ( - popGeoSource: PopGeoQualityFollowSource, - statsReceiver: StatsReceiver) - extends BasePopGeohashSource( - popGeoSource = popGeoSource, - statsReceiver = statsReceiver.scope("PopGeohashQualityFollowSource"), - ) { - override val identifier: CandidateSourceIdentifier = PopGeohashQualityFollowSource.Identifier - override def maxResults(target: Target): Int = { - target.params(PopGeoQualityFollowSourceParams.PopGeoSourceMaxResultsPerPrecision) - } - override def minGeohashLength(target: Target): Int = { - target.params(PopGeoQualityFollowSourceParams.PopGeoSourceGeoHashMinPrecision) - } - override def maxGeohashLength(target: Target): Int = { - target.params(PopGeoQualityFollowSourceParams.PopGeoSourceGeoHashMaxPrecision) - } - override def returnResultFromAllPrecision(target: Target): Boolean = { - target.params(PopGeoQualityFollowSourceParams.PopGeoSourceReturnFromAllPrecisions) - } - override def candidateSourceEnabled(target: Target): Boolean = { - target.params(PopGeoQualityFollowSourceParams.CandidateSourceEnabled) - } -} - -object PopGeohashQualityFollowSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.PopGeohashQualityFollow.toString) -} - -object PopGeoQualityFollowSource { - val MaxCacheSize = 20000 - val CacheTTL: Duration = Duration.fromHours(24) - val MaxResults = 200 -} - -@Singleton -class PopGeoQualityFollowSource @Inject() ( - popGeoQualityFollowClientColumn: UniquePopQualityFollowUsersInPlaceClientColumn, - statsReceiver: StatsReceiver, -) extends CandidateSource[String, CandidateUser] { - - /** @see [[CandidateSourceIdentifier]] */ - override val identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - "PopGeoQualityFollowSource") - - private val cache = StitchCache[String, Option[PopUsersInPlace]]( - maxCacheSize = PopGeoQualityFollowSource.MaxCacheSize, - ttl = PopGeoQualityFollowSource.CacheTTL, - statsReceiver = statsReceiver.scope(identifier.name, "cache"), - underlyingCall = (k: String) => { - popGeoQualityFollowClientColumn.fetcher - .fetch(k) - .map { result => result.v } - } - ) - - override def apply(target: String): Stitch[Seq[CandidateUser]] = { - val result: Stitch[Option[PopUsersInPlace]] = cache.readThrough(target) - result.map { pu => - pu.map { candidates => - candidates.popUsers.sortBy(-_.score).take(PopGeoQualityFollowSource.MaxResults).map { - candidate => - CandidateUser( - id = candidate.userId, - score = Some(candidate.score), - reason = Some( - Reason( - Some( - AccountProof( - popularInGeoProof = Some(PopularInGeoProof(location = candidates.place)) - ) - ) - ) - ) - ) - } - }.getOrElse(Nil) - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSourceFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSourceFSConfig.scala deleted file mode 100644 index 4d8577522..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSourceFSConfig.scala +++ /dev/null @@ -1,24 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.geo - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.FSParam -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PopGeoQualityFollowSourceFSConfig @Inject() () extends FeatureSwitchConfig { - override val intFSParams: Seq[FSBoundedParam[Int] with FSName] = Seq( - PopGeoQualityFollowSourceParams.PopGeoSourceGeoHashMaxPrecision, - PopGeoQualityFollowSourceParams.PopGeoSourceGeoHashMinPrecision, - PopGeoQualityFollowSourceParams.PopGeoSourceMaxResultsPerPrecision - ) - override val doubleFSParams: Seq[FSBoundedParam[Double] with FSName] = Seq( - PopGeoQualityFollowSourceParams.CandidateSourceWeight - ) - override val booleanFSParams: Seq[FSParam[Boolean] with FSName] = Seq( - PopGeoQualityFollowSourceParams.CandidateSourceEnabled, - PopGeoQualityFollowSourceParams.PopGeoSourceReturnFromAllPrecisions - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSourceParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSourceParams.scala deleted file mode 100644 index dac92df52..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoQualityFollowSourceParams.scala +++ /dev/null @@ -1,42 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.geo - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam - -object PopGeoQualityFollowSourceParams { - case object CandidateSourceEnabled - extends FSParam[Boolean]("pop_geo_quality_follow_source_enabled", false) - - case object PopGeoSourceGeoHashMinPrecision - extends FSBoundedParam[Int]( - "pop_geo_quality_follow_source_geo_hash_min_precision", - default = 2, - min = 0, - max = 10) - - case object PopGeoSourceGeoHashMaxPrecision - extends FSBoundedParam[Int]( - "pop_geo_quality_follow_source_geo_hash_max_precision", - default = 3, - min = 0, - max = 10) - - case object PopGeoSourceReturnFromAllPrecisions - extends FSParam[Boolean]( - "pop_geo_quality_follow_source_return_from_all_precisions", - default = false) - - case object PopGeoSourceMaxResultsPerPrecision - extends FSBoundedParam[Int]( - "pop_geo_quality_follow_source_max_results_per_precision", - default = 200, - min = 0, - max = 1000) - - case object CandidateSourceWeight - extends FSBoundedParam[Double]( - "pop_geo_quality_follow_source_weight", - default = 200, - min = 0.001, - max = 2000) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSource.scala deleted file mode 100644 index 2db13ed69..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSource.scala +++ /dev/null @@ -1,69 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.geo - -import com.google.inject.Singleton -import com.google.inject.name.Named -import com.twitter.conversions.DurationOps._ -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.candidate_sources.base.CachedCandidateSource -import com.twitter.follow_recommendations.common.candidate_sources.base.StratoFetcherWithUnitViewSource -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.PopularInGeoProof -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.hermit.pop_geo.thriftscala.PopUsersInPlace -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.client.Fetcher -import com.twitter.util.Duration -import javax.inject.Inject - -@Singleton -class BasePopGeoSource @Inject() ( - @Named(GuiceNamedConstants.POP_USERS_IN_PLACE_FETCHER) fetcher: Fetcher[ - String, - Unit, - PopUsersInPlace - ]) extends StratoFetcherWithUnitViewSource[String, PopUsersInPlace]( - fetcher, - BasePopGeoSource.Identifier) { - - override def map(target: String, candidates: PopUsersInPlace): Seq[CandidateUser] = - BasePopGeoSource.map(target, candidates) -} - -object BasePopGeoSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier("BasePopGeoSource") - val MaxResults = 200 - - def map(target: String, candidates: PopUsersInPlace): Seq[CandidateUser] = - candidates.popUsers.sortBy(-_.score).take(BasePopGeoSource.MaxResults).view.map { candidate => - CandidateUser( - id = candidate.userId, - score = Some(candidate.score), - reason = Some( - Reason( - Some( - AccountProof( - popularInGeoProof = Some(PopularInGeoProof(location = candidates.place)) - ) - ) - ) - ) - ) - } -} - -@Singleton -class PopGeoSource @Inject() (basePopGeoSource: BasePopGeoSource, statsReceiver: StatsReceiver) - extends CachedCandidateSource[String, CandidateUser]( - basePopGeoSource, - PopGeoSource.MaxCacheSize, - PopGeoSource.CacheTTL, - statsReceiver, - PopGeoSource.Identifier) - -object PopGeoSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier("PopGeoSource") - val MaxCacheSize = 20000 - val CacheTTL: Duration = 1.hours -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSourceFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSourceFSConfig.scala deleted file mode 100644 index ea3f6ce38..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSourceFSConfig.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.geo - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.FSParam -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PopGeoSourceFSConfig @Inject() () extends FeatureSwitchConfig { - override val intFSParams: Seq[FSBoundedParam[Int] with FSName] = Seq( - PopGeoSourceParams.PopGeoSourceGeoHashMaxPrecision, - PopGeoSourceParams.PopGeoSourceMaxResultsPerPrecision, - PopGeoSourceParams.PopGeoSourceGeoHashMinPrecision, - ) - override val booleanFSParams: Seq[FSParam[Boolean] with FSName] = Seq( - PopGeoSourceParams.PopGeoSourceReturnFromAllPrecisions, - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSourceParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSourceParams.scala deleted file mode 100644 index a63e320b4..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeoSourceParams.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.geo - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam - -object PopGeoSourceParams { - case object PopGeoSourceGeoHashMinPrecision - extends FSBoundedParam[Int]( - "pop_geo_source_geo_hash_min_precision", - default = 2, - min = 0, - max = 10) - - case object PopGeoSourceGeoHashMaxPrecision - extends FSBoundedParam[Int]( - "pop_geo_source_geo_hash_max_precision", - default = 4, - min = 0, - max = 10) - - case object PopGeoSourceReturnFromAllPrecisions - extends FSParam[Boolean]("pop_geo_source_return_from_all_precisions", default = false) - - case object PopGeoSourceMaxResultsPerPrecision - extends FSBoundedParam[Int]( - "pop_geo_source_max_results_per_precision", - default = 200, - min = 0, - max = 1000) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeohashSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeohashSource.scala deleted file mode 100644 index 9447b4867..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/PopGeohashSource.scala +++ /dev/null @@ -1,36 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.geo - -import com.google.inject.Singleton -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import javax.inject.Inject - -@Singleton -class PopGeohashSource @Inject() ( - popGeoSource: PopGeoSource, - statsReceiver: StatsReceiver) - extends BasePopGeohashSource( - popGeoSource = popGeoSource, - statsReceiver = statsReceiver.scope("PopGeohashSource"), - ) { - override def candidateSourceEnabled(target: Target): Boolean = true - override val identifier: CandidateSourceIdentifier = PopGeohashSource.Identifier - override def minGeohashLength(target: Target): Int = { - target.params(PopGeoSourceParams.PopGeoSourceGeoHashMinPrecision) - } - override def maxResults(target: Target): Int = { - target.params(PopGeoSourceParams.PopGeoSourceMaxResultsPerPrecision) - } - override def maxGeohashLength(target: Target): Int = { - target.params(PopGeoSourceParams.PopGeoSourceGeoHashMaxPrecision) - } - override def returnResultFromAllPrecision(target: Target): Boolean = { - target.params(PopGeoSourceParams.PopGeoSourceReturnFromAllPrecisions) - } -} - -object PopGeohashSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.PopGeohash.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/README.md deleted file mode 100644 index 13c2f245f..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Pop Geo Candidate Source -Provides the most followed / quality followed accounts in a specific country and a geolocation within past 2 weeks. -* A "quality follow" refers to any follow that leads to visible engagement, such as favorites, mentions, retweets, direct messages, replies, and quote tweets. The engagement must be allowed in either direction, and must occur on the day of the follow or within one subsequent day. Additionally, there must be no unfollowing, blocking, muting, or reporting of the account in the same time period. -* The minimum geolocation precision used is ±20 km (12 mi), and precise user geolocation is not utilized. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/BUILD deleted file mode 100644 index 0ff68490c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/BUILD +++ /dev/null @@ -1,23 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "src/thrift/com/twitter/hermit/candidate:hermit-candidate-scala", - "strato/config/columns/onboarding:onboarding-strato-client", - "strato/config/columns/onboarding/userrecs:userrecs-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSource.scala deleted file mode 100644 index e22fb465d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSource.scala +++ /dev/null @@ -1,84 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow.PPMILocaleFollowSourceParams.CandidateSourceEnabled -import com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow.PPMILocaleFollowSourceParams.LocaleToExcludeFromRecommendation -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch - -import javax.inject.Inject -import javax.inject.Singleton -import com.twitter.strato.generated.client.onboarding.UserPreferredLanguagesOnUserClientColumn -import com.twitter.strato.generated.client.onboarding.userrecs.LocaleFollowPpmiClientColumn -import com.twitter.timelines.configapi.HasParams - -/** - * Fetches candidates based on the Positive Pointwise Mutual Information (PPMI) statistic - * for a set of locales - * */ -@Singleton -class PPMILocaleFollowSource @Inject() ( - userPreferredLanguagesOnUserClientColumn: UserPreferredLanguagesOnUserClientColumn, - localeFollowPpmiClientColumn: LocaleFollowPpmiClientColumn, - statsReceiver: StatsReceiver) - extends CandidateSource[HasClientContext with HasParams, CandidateUser] { - - override val identifier: CandidateSourceIdentifier = PPMILocaleFollowSource.Identifier - private val stats = statsReceiver.scope("PPMILocaleFollowSource") - - override def apply(target: HasClientContext with HasParams): Stitch[Seq[CandidateUser]] = { - (for { - countryCode <- target.getCountryCode - userId <- target.getOptionalUserId - } yield { - getPreferredLocales(userId, countryCode.toLowerCase()) - .flatMap { locale => - stats.addGauge("allLocale") { - locale.length - } - val filteredLocale = - locale.filter(!target.params(LocaleToExcludeFromRecommendation).contains(_)) - stats.addGauge("postFilterLocale") { - filteredLocale.length - } - if (target.params(CandidateSourceEnabled)) { - getPPMILocaleFollowCandidates(filteredLocale) - } else Stitch(Seq.empty) - } - .map(_.sortBy(_.score)(Ordering[Option[Double]].reverse) - .take(PPMILocaleFollowSource.DefaultMaxCandidatesToReturn)) - }).getOrElse(Stitch.Nil) - } - - private def getPPMILocaleFollowCandidates( - locales: Seq[String] - ): Stitch[Seq[CandidateUser]] = { - Stitch - .traverse(locales) { locale => - // Get PPMI candidates for each locale - localeFollowPpmiClientColumn.fetcher - .fetch(locale) - .map(_.v - .map(_.candidates).getOrElse(Nil).map { candidate => - CandidateUser(id = candidate.userId, score = Some(candidate.score)) - }.map(_.withCandidateSource(identifier))) - }.map(_.flatten) - } - - private def getPreferredLocales(userId: Long, countryCode: String): Stitch[Seq[String]] = { - userPreferredLanguagesOnUserClientColumn.fetcher - .fetch(userId) - .map(_.v.map(_.languages).getOrElse(Nil).map { lang => - s"$countryCode-$lang".toLowerCase - }) - } -} - -object PPMILocaleFollowSource { - val Identifier = CandidateSourceIdentifier(Algorithm.PPMILocaleFollow.toString) - val DefaultMaxCandidatesToReturn = 100 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSourceFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSourceFSConfig.scala deleted file mode 100644 index 8a40ca92d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSourceFSConfig.scala +++ /dev/null @@ -1,24 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.Param - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PPMILocaleFollowSourceFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = Seq( - PPMILocaleFollowSourceParams.CandidateSourceEnabled, - ) - - override val stringSeqFSParams: Seq[Param[Seq[String]] with FSName] = Seq( - PPMILocaleFollowSourceParams.LocaleToExcludeFromRecommendation, - ) - - override val doubleFSParams: Seq[FSBoundedParam[Double]] = Seq( - PPMILocaleFollowSourceParams.CandidateSourceWeight, - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSourceParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSourceParams.scala deleted file mode 100644 index 18aac545d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/PPMILocaleFollowSourceParams.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam - -class PPMILocaleFollowSourceParams {} -object PPMILocaleFollowSourceParams { - case object LocaleToExcludeFromRecommendation - extends FSParam[Seq[String]]( - "ppmilocale_follow_source_locales_to_exclude_from_recommendation", - default = Seq.empty) - - case object CandidateSourceEnabled - extends FSParam[Boolean]("ppmilocale_follow_source_enabled", true) - - case object CandidateSourceWeight - extends FSBoundedParam[Double]( - "ppmilocale_follow_source_candidate_source_weight", - default = 1, - min = 0.001, - max = 2000) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/README.md deleted file mode 100644 index 48cbee112..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# PPMI Locale Follow Candidate Source -Provides accounts based on PPMI ([Positive Pointwise Mutual Information](https://en.wikipedia.org/wiki/Pointwise_mutual_information#Positive_PMI)) using follow actions as a feature for a specific local (language + country) within a week. In simpler terms, it provides a list of the most followed accounts for a given country and language input, based on the PPMI algorithm. - -PPMI is a statistical measure of the association between two events. In this case, it measures the association between the follow actions and the accounts being followed. - -In summary, the service utilizes PPMI and follow actions to provide a list of the most followed accounts for a specific country and language input. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/BUILD deleted file mode 100644 index 27caedd22..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/BUILD +++ /dev/null @@ -1,11 +0,0 @@ -scala_library( - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "src/thrift/com/twitter/socialgraph:thrift-scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/PromotedAccountsCandidateSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/PromotedAccountsCandidateSource.scala deleted file mode 100644 index ff2ad4cd9..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/PromotedAccountsCandidateSource.scala +++ /dev/null @@ -1,111 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.promoted_accounts - -import com.twitter.adserver.thriftscala.AdServerException -import com.twitter.adserver.{thriftscala => adthrift} -import com.twitter.finagle.TimeoutException -import com.twitter.finagle.stats.Counter -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.clients.adserver.AdRequest -import com.twitter.follow_recommendations.common.clients.adserver.AdserverClient -import com.twitter.follow_recommendations.common.clients.socialgraph.SocialGraphClient -import com.twitter.follow_recommendations.common.models.FollowProof -import com.twitter.hermit.model.Algorithm -import com.twitter.inject.Logging -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -case class PromotedCandidateUser( - id: Long, - position: Int, - adImpression: adthrift.AdImpression, - followProof: FollowProof, - primaryCandidateSource: Option[CandidateSourceIdentifier]) - -@Singleton -class PromotedAccountsCandidateSource @Inject() ( - adserverClient: AdserverClient, - sgsClient: SocialGraphClient, - statsReceiver: StatsReceiver) - extends CandidateSource[AdRequest, PromotedCandidateUser] - with Logging { - - override val identifier: CandidateSourceIdentifier = - PromotedAccountsCandidateSource.Identifier - - val stats: StatsReceiver = statsReceiver.scope(identifier.name) - val failureStat: StatsReceiver = stats.scope("failures") - val adServerExceptionsCounter: Counter = failureStat.counter("AdServerException") - val timeoutCounter: Counter = failureStat.counter("TimeoutException") - - def apply(request: AdRequest): Stitch[Seq[PromotedCandidateUser]] = { - adserverClient - .getAdImpressions(request) - .rescue { - case e: TimeoutException => - timeoutCounter.incr() - logger.warn("Timeout on Adserver", e) - Stitch.Nil - case e: AdServerException => - adServerExceptionsCounter.incr() - logger.warn("Failed to fetch ads", e) - Stitch.Nil - } - .flatMap { adImpressions: Seq[adthrift.AdImpression] => - profileNumResults(adImpressions.size, "results_from_ad_server") - val idToImpMap = (for { - imp <- adImpressions - promotedAccountId <- imp.promotedAccountId - } yield promotedAccountId -> imp).toMap - request.clientContext.userId - .map { userId => - sgsClient - .getIntersections( - userId, - adImpressions.filter(shouldShowSocialContext).flatMap(_.promotedAccountId), - PromotedAccountsCandidateSource.NumIntersections - ).map { promotedAccountWithIntersections => - idToImpMap.map { - case (promotedAccountId, imp) => - PromotedCandidateUser( - promotedAccountId, - imp.insertionPosition - .map(_.toInt).getOrElse( - getInsertionPositionDefaultValue(request.isTest.getOrElse(false)) - ), - imp, - promotedAccountWithIntersections - .getOrElse(promotedAccountId, FollowProof(Nil, 0)), - Some(identifier) - ) - }.toSeq - }.onSuccess(result => profileNumResults(result.size, "final_results")) - }.getOrElse(Stitch.Nil) - } - } - - private def shouldShowSocialContext(imp: adthrift.AdImpression): Boolean = - imp.experimentValues.exists { expValues => - expValues.get("display.display_style").contains("show_social_context") - } - - private def getInsertionPositionDefaultValue(isTest: Boolean): Int = { - if (isTest) 0 else -1 - } - - private def profileNumResults(resultsSize: Int, statName: String): Unit = { - if (resultsSize <= 5) { - stats.scope(statName).counter(resultsSize.toString).incr() - } else { - stats.scope(statName).counter("more_than_5").incr() - } - } -} - -object PromotedAccountsCandidateSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.PromotedAccount.toString) - val NumIntersections = 3 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/README.md deleted file mode 100644 index 1091e1d88..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Promoted Accounts Candidate Source -Promoted accounts returned from Ads server. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/BUILD deleted file mode 100644 index 07c3c6665..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/BUILD +++ /dev/null @@ -1,24 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/stores", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "strato/config/columns/onboarding/realGraph:realGraph-strato-client", - "strato/config/columns/onboarding/userrecs:userrecs-strato-client", - "strato/config/columns/recommendations/twistly:twistly-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/README.md deleted file mode 100644 index 4ba5a8c11..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# RealGraph Candidate Source -Provides out-of-network RealGraph candidates for a given user. RealGraph is a user-user graph dataset that aims to measure the strength of the relationship between two users. - -RealGraph comprises two components: a real-time pipeline that tracks various counts and relationships between user-user edges (such as the number of favorites, replies, retweets, clicks, whether followed, muted, or blocked), and an offline pipeline of a larger set of such user-user edge counts and relationships. Currently, the top k in-network scores have been exported for use by various teams. - -The RealGraph dataset is used to predict user interactions at Twitter, and is based on the paper "[Realgraph: User interaction prediction at Twitter](http://www.ueo-workshop.com/wp-content/uploads/2014/04/sig-alternate.pdf)" by the UEO workshop at KDD'14. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonFSConfig.scala deleted file mode 100644 index df9c8ac68..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonFSConfig.scala +++ /dev/null @@ -1,27 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.real_graph - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.Param -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RealGraphOonFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = - Seq( - RealGraphOonParams.IncludeRealGraphOonCandidates, - RealGraphOonParams.TryToReadRealGraphOonCandidates, - RealGraphOonParams.UseV2 - ) - override val doubleFSParams: Seq[FSBoundedParam[Double]] = - Seq( - RealGraphOonParams.ScoreThreshold - ) - override val intFSParams: Seq[FSBoundedParam[Int]] = - Seq( - RealGraphOonParams.RealGraphOonResultCountThreshold, - RealGraphOonParams.MaxResults, - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonParams.scala deleted file mode 100644 index feaa1d7c7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonParams.scala +++ /dev/null @@ -1,47 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.real_graph - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam - -object RealGraphOonParams { - case object IncludeRealGraphOonCandidates - extends FSParam[Boolean]( - "real_graph_oon_include_candidates", - false - ) - case object TryToReadRealGraphOonCandidates - extends FSParam[Boolean]( - "real_graph_oon_try_to_read_candidates", - false - ) - case object RealGraphOonResultCountThreshold - extends FSBoundedParam[Int]( - "real_graph_oon_result_count_threshold", - default = 1, - min = 0, - max = Integer.MAX_VALUE - ) - - case object UseV2 - extends FSParam[Boolean]( - "real_graph_oon_use_v2", - false - ) - - case object ScoreThreshold - extends FSBoundedParam[Double]( - "real_graph_oon_score_threshold", - default = 0.26, - min = 0, - max = 1.0 - ) - - case object MaxResults - extends FSBoundedParam[Int]( - "real_graph_oon_max_results", - default = 200, - min = 0, - max = 1000 - ) - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonV2Source.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonV2Source.scala deleted file mode 100644 index 3c709770c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphOonV2Source.scala +++ /dev/null @@ -1,58 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.real_graph - -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.realGraph.UserRealgraphOonV2ClientColumn -import com.twitter.timelines.configapi.HasParams -import com.twitter.wtf.candidate.thriftscala.CandidateSeq -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RealGraphOonV2Source @Inject() ( - realGraphClientColumn: UserRealgraphOonV2ClientColumn) - extends CandidateSource[HasParams with HasClientContext, CandidateUser] { - - override val identifier: CandidateSourceIdentifier = - RealGraphOonV2Source.Identifier - - override def apply(request: HasParams with HasClientContext): Stitch[Seq[CandidateUser]] = { - request.getOptionalUserId - .map { userId => - realGraphClientColumn.fetcher - .fetch(userId) - .map { result => - result.v - .map { candidates => parseStratoResults(request, candidates) } - .getOrElse(Nil) - // returned candidates are sorted by score in descending order - .take(request.params(RealGraphOonParams.MaxResults)) - .map(_.withCandidateSource(identifier)) - } - }.getOrElse(Stitch(Seq.empty)) - } - - private def parseStratoResults( - request: HasParams with HasClientContext, - candidateSeqThrift: CandidateSeq - ): Seq[CandidateUser] = { - candidateSeqThrift.candidates.collect { - case candidate if candidate.score >= request.params(RealGraphOonParams.ScoreThreshold) => - CandidateUser( - candidate.userId, - Some(candidate.score) - ) - } - } - -} - -object RealGraphOonV2Source { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.RealGraphOonV2.toString - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphSource.scala deleted file mode 100644 index 7aa2aa8af..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph/RealGraphSource.scala +++ /dev/null @@ -1,40 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.real_graph - -import com.twitter.follow_recommendations.common.clients.real_time_real_graph.RealTimeRealGraphClient -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject -import javax.inject.Singleton - -/** - * This source gets the already followed edges from the real graph column as a candidate source. - */ -@Singleton -class RealGraphSource @Inject() ( - realGraph: RealTimeRealGraphClient) - extends CandidateSource[HasParams with HasClientContext, CandidateUser] { - override val identifier: CandidateSourceIdentifier = RealGraphSource.Identifier - - override def apply(request: HasParams with HasClientContext): Stitch[Seq[CandidateUser]] = { - request.getOptionalUserId - .map { userId => - realGraph.getRealGraphWeights(userId).map { scoreMap => - scoreMap.map { - case (candidateId, realGraphScore) => - CandidateUser(id = candidateId, score = Some(realGraphScore)) - .withCandidateSource(identifier) - }.toSeq - } - }.getOrElse(Stitch.Nil) - } -} - -object RealGraphSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.RealGraphFollowed.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/BUILD deleted file mode 100644 index b2764c42e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/BUILD +++ /dev/null @@ -1,29 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "discovery-ds/src/main/thrift/com/twitter/dds/jobs/repeated_profile_visits:profile_visit-scala", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "hermit/hermit-core/src/main/scala/com/twitter/hermit/model", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common/identifier", - "src/thrift/com/twitter/experiments/general_metrics:general_metrics-scala", - "strato/config/columns/rux:rux-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/README.md deleted file mode 100644 index 616a3f7ed..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# Recent Engagement Candidate Source -Provides recently engaged accounts for a given user: -* Explicit engagements: like, retweet, reply -* Implicit engagements: profile visit diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RecentEngagementDirectFollowSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RecentEngagementDirectFollowSource.scala deleted file mode 100644 index edfaac5e6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RecentEngagementDirectFollowSource.scala +++ /dev/null @@ -1,38 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.recent_engagement - -import com.twitter.follow_recommendations.common.clients.real_time_real_graph.RealTimeRealGraphClient -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RecentEngagementDirectFollowSource @Inject() ( - realTimeRealGraphClient: RealTimeRealGraphClient) - extends CandidateSource[Long, CandidateUser] { - - val identifier: CandidateSourceIdentifier = - RecentEngagementDirectFollowSource.Identifier - - /** - * Generate a list of candidates for the target using RealtimeGraphClient - * and RecentEngagementStore. - */ - override def apply(targetUserId: Long): Stitch[Seq[CandidateUser]] = { - realTimeRealGraphClient - .getUsersRecentlyEngagedWith( - userId = targetUserId, - engagementScoreMap = RealTimeRealGraphClient.EngagementScoreMap, - includeDirectFollowCandidates = true, - includeNonDirectFollowCandidates = false - ) - .map(_.map(_.withCandidateSource(identifier)).sortBy(-_.score.getOrElse(0.0))) - } -} - -object RecentEngagementDirectFollowSource { - val Identifier = CandidateSourceIdentifier(Algorithm.RecentEngagementDirectFollow.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RecentEngagementNonDirectFollowSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RecentEngagementNonDirectFollowSource.scala deleted file mode 100644 index 46572da71..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RecentEngagementNonDirectFollowSource.scala +++ /dev/null @@ -1,38 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.recent_engagement - -import com.twitter.follow_recommendations.common.clients.real_time_real_graph.RealTimeRealGraphClient -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RecentEngagementNonDirectFollowSource @Inject() ( - realTimeRealGraphClient: RealTimeRealGraphClient) - extends CandidateSource[Long, CandidateUser] { - - val identifier: CandidateSourceIdentifier = - RecentEngagementNonDirectFollowSource.Identifier - - /** - * Generate a list of candidates for the target using RealtimeGraphClient - * and RecentEngagementStore. - */ - override def apply(targetUserId: Long): Stitch[Seq[CandidateUser]] = { - realTimeRealGraphClient - .getUsersRecentlyEngagedWith( - userId = targetUserId, - engagementScoreMap = RealTimeRealGraphClient.EngagementScoreMap, - includeDirectFollowCandidates = false, - includeNonDirectFollowCandidates = true - ) - .map(_.map(_.withCandidateSource(identifier)).sortBy(-_.score.getOrElse(0.0))) - } -} - -object RecentEngagementNonDirectFollowSource { - val Identifier = CandidateSourceIdentifier(Algorithm.RecentEngagementNonDirectFollow.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsFSConfig.scala deleted file mode 100644 index 5aaccae2b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsFSConfig.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.recent_engagement - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.Param -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RepeatedProfileVisitsFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = - Seq( - RepeatedProfileVisitsParams.IncludeCandidates, - RepeatedProfileVisitsParams.UseOnlineDataset, - ) - override val intFSParams: Seq[FSBoundedParam[Int]] = - Seq( - RepeatedProfileVisitsParams.RecommendationThreshold, - RepeatedProfileVisitsParams.BucketingThreshold, - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsParams.scala deleted file mode 100644 index 5402d38f4..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsParams.scala +++ /dev/null @@ -1,37 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.recent_engagement - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam - -object RepeatedProfileVisitsParams { - - // If RepeatedProfileVisitsSource is run and there are recommended candidates for the target user, whether or not - // to actually include such candidates in our output recommendations. This FS will be used to control bucketing of - // users into control vs treatment buckets. - case object IncludeCandidates - extends FSParam[Boolean](name = "repeated_profile_visits_include_candidates", default = false) - - // The threshold at or above which we will consider a profile to have been visited "frequently enough" to recommend - // the profile to the target user. - case object RecommendationThreshold - extends FSBoundedParam[Int]( - name = "repeated_profile_visits_recommendation_threshold", - default = 3, - min = 0, - max = Integer.MAX_VALUE) - - // The threshold at or above which we will consider a profile to have been visited "frequently enough" to recommend - // the profile to the target user. - case object BucketingThreshold - extends FSBoundedParam[Int]( - name = "repeated_profile_visits_bucketing_threshold", - default = 3, - min = 0, - max = Integer.MAX_VALUE) - - // Whether or not to use the online dataset (which has repeated profile visits information updated to within minutes) - // instead of the offline dataset (updated via offline jobs, which can have delays of hours to days). - case object UseOnlineDataset - extends FSParam[Boolean](name = "repeated_profile_visits_use_online_dataset", default = true) - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsSource.scala deleted file mode 100644 index c4b4aa3e7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement/RepeatedProfileVisitsSource.scala +++ /dev/null @@ -1,157 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.recent_engagement - -import com.google.inject.Inject -import com.google.inject.Singleton -import com.twitter.dds.jobs.repeated_profile_visits.thriftscala.ProfileVisitorInfo -import com.twitter.experiments.general_metrics.thriftscala.IdType -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.clients.real_time_real_graph.Engagement -import com.twitter.follow_recommendations.common.clients.real_time_real_graph.RealTimeRealGraphClient -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Params -import com.twitter.hermit.model.Algorithm -import com.twitter.inject.Logging -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.rux.RepeatedProfileVisitsAggregateClientColumn - -@Singleton -class RepeatedProfileVisitsSource @Inject() ( - repeatedProfileVisitsAggregateClientColumn: RepeatedProfileVisitsAggregateClientColumn, - realTimeRealGraphClient: RealTimeRealGraphClient, - statsReceiver: StatsReceiver) - extends CandidateSource[HasParams with HasClientContext, CandidateUser] - with Logging { - - val identifier: CandidateSourceIdentifier = - RepeatedProfileVisitsSource.Identifier - - val sourceStatsReceiver = statsReceiver.scope("repeated_profile_visits_source") - val offlineFetchErrorCounter = sourceStatsReceiver.counter("offline_fetch_error") - val offlineFetchSuccessCounter = sourceStatsReceiver.counter("offline_fetch_success") - val onlineFetchErrorCounter = sourceStatsReceiver.counter("online_fetch_error") - val onlineFetchSuccessCounter = sourceStatsReceiver.counter("online_fetch_success") - val noRepeatedProfileVisitsAboveBucketingThresholdCounter = - sourceStatsReceiver.counter("no_repeated_profile_visits_above_bucketing_threshold") - val hasRepeatedProfileVisitsAboveBucketingThresholdCounter = - sourceStatsReceiver.counter("has_repeated_profile_visits_above_bucketing_threshold") - val noRepeatedProfileVisitsAboveRecommendationsThresholdCounter = - sourceStatsReceiver.counter("no_repeated_profile_visits_above_recommendations_threshold") - val hasRepeatedProfileVisitsAboveRecommendationsThresholdCounter = - sourceStatsReceiver.counter("has_repeated_profile_visits_above_recommendations_threshold") - val includeCandidatesCounter = sourceStatsReceiver.counter("include_candidates") - val noIncludeCandidatesCounter = sourceStatsReceiver.counter("no_include_candidates") - - // Returns visited user -> visit count, via off dataset. - def applyWithOfflineDataset(targetUserId: Long): Stitch[Map[Long, Int]] = { - repeatedProfileVisitsAggregateClientColumn.fetcher - .fetch(ProfileVisitorInfo(id = targetUserId, idType = IdType.User)).map(_.v) - .handle { - case e: Throwable => - logger.error("Strato fetch for RepeatedProfileVisitsAggregateClientColumn failed: " + e) - offlineFetchErrorCounter.incr() - None - }.onSuccess { result => - offlineFetchSuccessCounter.incr() - }.map { resultOption => - resultOption - .flatMap { result => - result.profileVisitSet.map { profileVisitSet => - profileVisitSet - .filter(profileVisit => profileVisit.totalTargetVisitsInLast14Days.getOrElse(0) > 0) - .filter(profileVisit => !profileVisit.doesSourceIdFollowTargetId.getOrElse(false)) - .flatMap { profileVisit => - (profileVisit.targetId, profileVisit.totalTargetVisitsInLast14Days) match { - case (Some(targetId), Some(totalVisitsInLast14Days)) => - Some(targetId -> totalVisitsInLast14Days) - case _ => None - } - }.toMap[Long, Int] - } - }.getOrElse(Map.empty) - } - } - - // Returns visited user -> visit count, via online dataset. - def applyWithOnlineData(targetUserId: Long): Stitch[Map[Long, Int]] = { - val visitedUserToEngagementsStitch: Stitch[Map[Long, Seq[Engagement]]] = - realTimeRealGraphClient.getRecentProfileViewEngagements(targetUserId) - visitedUserToEngagementsStitch - .onFailure { f => - onlineFetchErrorCounter.incr() - }.onSuccess { result => - onlineFetchSuccessCounter.incr() - }.map { visitedUserToEngagements => - visitedUserToEngagements - .mapValues(engagements => engagements.size) - } - } - - def getRepeatedVisitedAccounts(params: Params, targetUserId: Long): Stitch[Map[Long, Int]] = { - var results: Stitch[Map[Long, Int]] = Stitch.value(Map.empty) - if (params.getBoolean(RepeatedProfileVisitsParams.UseOnlineDataset)) { - results = applyWithOnlineData(targetUserId) - } else { - results = applyWithOfflineDataset(targetUserId) - } - // Only keep users that had non-zero engagement counts. - results.map(_.filter(input => input._2 > 0)) - } - - def getRecommendations(params: Params, userId: Long): Stitch[Seq[CandidateUser]] = { - val recommendationThreshold = params.getInt(RepeatedProfileVisitsParams.RecommendationThreshold) - val bucketingThreshold = params.getInt(RepeatedProfileVisitsParams.BucketingThreshold) - - // Get the list of repeatedly visited profilts. Only keep accounts with >= bucketingThreshold visits. - val repeatedVisitedAccountsStitch: Stitch[Map[Long, Int]] = - getRepeatedVisitedAccounts(params, userId).map(_.filter(kv => kv._2 >= bucketingThreshold)) - - repeatedVisitedAccountsStitch.map { candidates => - // Now check if we should includeCandidates (e.g. whether user is in control bucket or treatment buckets). - if (candidates.isEmpty) { - // User has not visited any accounts above bucketing threshold. We will not bucket user into experiment. Just - // don't return no candidates. - noRepeatedProfileVisitsAboveBucketingThresholdCounter.incr() - Seq.empty - } else { - hasRepeatedProfileVisitsAboveBucketingThresholdCounter.incr() - if (!params.getBoolean(RepeatedProfileVisitsParams.IncludeCandidates)) { - // User has reached bucketing criteria. We check whether to include candidates (e.g. checking which bucket - // the user is in for the experiment). In this case the user is in a bucket to not include any candidates. - noIncludeCandidatesCounter.incr() - Seq.empty - } else { - includeCandidatesCounter.incr() - // We should include candidates. Include any candidates above recommendation thresholds. - val outputCandidatesSeq = candidates - .filter(kv => kv._2 >= recommendationThreshold).map { kv => - val user = kv._1 - val visitCount = kv._2 - CandidateUser(user, Some(visitCount.toDouble)) - .withCandidateSource(RepeatedProfileVisitsSource.Identifier) - }.toSeq - if (outputCandidatesSeq.isEmpty) { - noRepeatedProfileVisitsAboveRecommendationsThresholdCounter.incr() - } else { - hasRepeatedProfileVisitsAboveRecommendationsThresholdCounter.incr() - } - outputCandidatesSeq - } - } - } - } - - override def apply(request: HasParams with HasClientContext): Stitch[Seq[CandidateUser]] = { - request.getOptionalUserId - .map { userId => - getRecommendations(request.params, userId) - }.getOrElse(Stitch.Nil) - } -} - -object RepeatedProfileVisitsSource { - val Identifier = CandidateSourceIdentifier(Algorithm.RepeatedProfileVisits.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/BUILD deleted file mode 100644 index 78a5729e2..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/BUILD +++ /dev/null @@ -1,21 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "src/thrift/com/twitter/onboarding/relevance/candidates:candidates-scala", - "strato/config/columns/onboarding/userrecs:userrecs-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/README.md deleted file mode 100644 index fb6d032d8..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# SALSA Candidate Source -Provides an account expansion based on the SALSA PYMK (People You May Know) algorithm for a given account. The algorithm focuses on the mutual follow and address book graph, making it highly effective at providing good mutual follow recommendations. - -The SALSA algorithm constructs a local graph and performs personalized random walks to identify the best recommendations for the user. The local graph represents the community of users that are most similar to or most relevant to the user, while the personalized random walk identifies the most popular interests among them. - -For each target user, the local graph is a bipartite graph with a left-hand side (LHS) and a right-hand side (RHS). The LHS is built from several sources, including the target user, forward and reverse address books, mutual follows, recent followings, and recent followers. We choose a specified number of top candidates from these sources for each target user with different weights assigned to each source to favor the corresponding source, and build the LHS using the target user and those top candidates. The RHS consists of two parts: the top candidates from the sources mentioned above for the target user and the mutual follows of the other entries in the LHS. - -The random walk starts from the target user in the LHS and adopts a restarting strategy to realize personalization. - -In summary, the SALSA Candidate Source provides an account expansion based on the SALSA PYMK algorithm, utilizing a bipartite graph with personalized random walks to identify the most relevant and interesting recommendations for the user. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/RecentEngagementDirectFollowSalsaExpansionSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/RecentEngagementDirectFollowSalsaExpansionSource.scala deleted file mode 100644 index 91fbba2ba..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/RecentEngagementDirectFollowSalsaExpansionSource.scala +++ /dev/null @@ -1,40 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.salsa - -import com.twitter.follow_recommendations.common.clients.real_time_real_graph.RealTimeRealGraphClient -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RecentEngagementDirectFollowSalsaExpansionSource @Inject() ( - realTimeRealGraphClient: RealTimeRealGraphClient, - salsaExpander: SalsaExpander) - extends SalsaExpansionBasedCandidateSource[Long](salsaExpander) { - - override val identifier: CandidateSourceIdentifier = - RecentEngagementDirectFollowSalsaExpansionSource.Identifier - - override def firstDegreeNodes(target: Long): Stitch[Seq[Long]] = realTimeRealGraphClient - .getUsersRecentlyEngagedWith( - target, - RealTimeRealGraphClient.EngagementScoreMap, - includeDirectFollowCandidates = true, - includeNonDirectFollowCandidates = false - ).map { recentlyFollowed => - recentlyFollowed - .take(RecentEngagementDirectFollowSalsaExpansionSource.NumFirstDegreeNodesToRetrieve) - .map(_.id) - } - - override def maxResults(target: Long): Int = - RecentEngagementDirectFollowSalsaExpansionSource.OutputSize -} - -object RecentEngagementDirectFollowSalsaExpansionSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.RecentEngagementSarusOcCur.toString) - val NumFirstDegreeNodesToRetrieve = 10 - val OutputSize = 200 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/SalsaExpander.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/SalsaExpander.scala deleted file mode 100644 index a9390826e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/SalsaExpander.scala +++ /dev/null @@ -1,117 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.salsa - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.strato.generated.client.onboarding.userrecs.SalsaFirstDegreeOnUserClientColumn -import com.twitter.strato.generated.client.onboarding.userrecs.SalsaSecondDegreeOnUserClientColumn -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FollowProof -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.stitch.Stitch -import com.twitter.wtf.candidate.thriftscala.Candidate -import javax.inject.Inject -import javax.inject.Singleton - -case class SalsaExpandedCandidate( - candidateId: Long, - numberOfConnections: Int, - totalScore: Double, - connectingUsers: Seq[Long]) { - def toCandidateUser: CandidateUser = - CandidateUser( - id = candidateId, - score = Some(totalScore), - reason = Some(Reason( - Some(AccountProof(followProof = Some(FollowProof(connectingUsers, connectingUsers.size)))))) - ) -} - -case class SimilarUserCandidate(candidateId: Long, score: Double, similarToCandidate: Long) - -/** - * Salsa expander uses pre-computed lists of candidates for each input user id and returns the highest scored candidates in the pre-computed lists as the expansion for the corresponding input id. - */ -@Singleton -class SalsaExpander @Inject() ( - statsReceiver: StatsReceiver, - firstDegreeClient: SalsaFirstDegreeOnUserClientColumn, - secondDegreeClient: SalsaSecondDegreeOnUserClientColumn, -) { - - val stats = statsReceiver.scope("salsa_expander") - - private def similarUsers( - input: Seq[Long], - neighbors: Seq[Option[Seq[Candidate]]] - ): Seq[SalsaExpandedCandidate] = { - input - .zip(neighbors).flatMap { - case (recId, Some(neighbors)) => - neighbors.map(neighbor => SimilarUserCandidate(neighbor.userId, neighbor.score, recId)) - case _ => Nil - }.groupBy(_.candidateId).map { - case (key, neighbors) => - val scores = neighbors.map(_.score) - val connectingUsers = neighbors - .sortBy(-_.score) - .take(SalsaExpander.MaxConnectingUsersToOutputPerExpandedCandidate) - .map(_.similarToCandidate) - - SalsaExpandedCandidate(key, scores.size, scores.sum, connectingUsers) - } - .filter( - _.numberOfConnections >= math - .min(SalsaExpander.MinConnectingUsersThreshold, input.size) - ) - .toSeq - } - - def apply( - firstDegreeInput: Seq[Long], - secondDegreeInput: Seq[Long], - maxNumOfCandidatesToReturn: Int - ): Stitch[Seq[CandidateUser]] = { - - val firstDegreeNeighborsStitch = - Stitch - .collect(firstDegreeInput.map(firstDegreeClient.fetcher - .fetch(_).map(_.v.map(_.candidates.take(SalsaExpander.MaxDirectNeighbors))))).onSuccess { - firstDegreeNeighbors => - stats.stat("first_degree_neighbors").add(firstDegreeNeighbors.flatten.size) - } - - val secondDegreeNeighborsStitch = - Stitch - .collect( - secondDegreeInput.map( - secondDegreeClient.fetcher - .fetch(_).map( - _.v.map(_.candidates.take(SalsaExpander.MaxIndirectNeighbors))))).onSuccess { - secondDegreeNeighbors => - stats.stat("second_degree_neighbors").add(secondDegreeNeighbors.flatten.size) - } - - val neighborStitches = - Stitch.join(firstDegreeNeighborsStitch, secondDegreeNeighborsStitch).map { - case (first, second) => first ++ second - } - - val similarUsersToInput = neighborStitches.map { neighbors => - similarUsers(firstDegreeInput ++ secondDegreeInput, neighbors) - } - - similarUsersToInput.map { - // Rank the candidate cot users by the combined weights from the connecting users. This is the default original implementation. It is unlikely to have weight ties and thus a second ranking function is not necessary. - _.sortBy(-_.totalScore) - .take(maxNumOfCandidatesToReturn) - .map(_.toCandidateUser) - } - } -} - -object SalsaExpander { - val MaxDirectNeighbors = 2000 - val MaxIndirectNeighbors = 2000 - val MinConnectingUsersThreshold = 2 - val MaxConnectingUsersToOutputPerExpandedCandidate = 3 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/SalsaExpansionBasedCandidateSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/SalsaExpansionBasedCandidateSource.scala deleted file mode 100644 index b299b966d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa/SalsaExpansionBasedCandidateSource.scala +++ /dev/null @@ -1,32 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.salsa - -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.stitch.Stitch - -abstract class SalsaExpansionBasedCandidateSource[Target](salsaExpander: SalsaExpander) - extends CandidateSource[Target, CandidateUser] { - - // Define first/second degree as empty sequences in cases of subclasses - // that don't implement one or the other. - // Example: MagicRecs only uses first degree nodes, and can ignore implementing secondDegreeNodes - // - // This allows apply(target) to combine both in the base class - def firstDegreeNodes(target: Target): Stitch[Seq[Long]] = Stitch.value(Seq()) - - def secondDegreeNodes(target: Target): Stitch[Seq[Long]] = Stitch.value(Seq()) - - // max number output results - def maxResults(target: Target): Int - - override def apply(target: Target): Stitch[Seq[CandidateUser]] = { - val nodes = Stitch.join(firstDegreeNodes(target), secondDegreeNodes(target)) - - nodes.flatMap { - case (firstDegreeCandidates, secondDegreeCandidates) => { - salsaExpander(firstDegreeCandidates, secondDegreeCandidates, maxResults(target)) - .map(_.map(_.withCandidateSource(identifier)).sortBy(-_.score.getOrElse(0.0))) - } - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/BUILD deleted file mode 100644 index 15c4eb94d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/BUILD +++ /dev/null @@ -1,24 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "src/thrift/com/twitter/hermit/candidate:hermit-candidate-scala", - "strato/config/columns/onboarding/userrecs:userrecs-strato-client", - "strato/config/columns/recommendations/follow2vec:follow2vec-strato-client", - "strato/config/columns/recommendations/similarity:similarity-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/CacheBasedSimsStore.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/CacheBasedSimsStore.scala deleted file mode 100644 index 03894b533..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/CacheBasedSimsStore.scala +++ /dev/null @@ -1,50 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims - -import com.twitter.escherbird.util.stitchcache.StitchCache -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.hermit.candidate.thriftscala.Candidates -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import com.twitter.strato.client.Fetcher -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.Duration - -import java.lang.{Long => JLong} - -class CacheBasedSimsStore( - id: CandidateSourceIdentifier, - fetcher: Fetcher[Long, Unit, Candidates], - maxCacheSize: Int, - cacheTtl: Duration, - statsReceiver: StatsReceiver) - extends CandidateSource[HasParams with HasSimilarToContext, CandidateUser] { - - override val identifier: CandidateSourceIdentifier = id - private def getUsersFromSimsSource(userId: JLong): Stitch[Option[Candidates]] = { - fetcher - .fetch(userId) - .map(_.v) - } - - private val simsCache = StitchCache[JLong, Option[Candidates]]( - maxCacheSize = maxCacheSize, - ttl = cacheTtl, - statsReceiver = statsReceiver, - underlyingCall = getUsersFromSimsSource - ) - - override def apply(request: HasParams with HasSimilarToContext): Stitch[Seq[CandidateUser]] = { - Stitch - .traverse(request.similarToUserIds) { userId => - simsCache.readThrough(userId).map { candidatesOpt => - candidatesOpt - .map { candidates => - StratoBasedSimsCandidateSource.map(userId, candidates) - }.getOrElse(Nil) - } - }.map(_.flatten.distinct.map(_.withCandidateSource(identifier))) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/DBV2SimsRefreshStore.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/DBV2SimsRefreshStore.scala deleted file mode 100644 index 3d59e6e0d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/DBV2SimsRefreshStore.scala +++ /dev/null @@ -1,35 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims - -import com.google.inject.Singleton -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.generated.client.onboarding.userrecs.NewSimsRefreshOnUserClientColumn -import com.twitter.util.Duration - -import javax.inject.Inject - -@Singleton -class DBV2SimsRefreshStore @Inject() ( - newSimsRefreshOnUserClientColumn: NewSimsRefreshOnUserClientColumn) - extends StratoBasedSimsCandidateSourceWithUnitView( - fetcher = newSimsRefreshOnUserClientColumn.fetcher, - identifier = DBV2SimsRefreshStore.Identifier) - -@Singleton -class CachedDBV2SimsRefreshStore @Inject() ( - newSimsRefreshOnUserClientColumn: NewSimsRefreshOnUserClientColumn, - statsReceiver: StatsReceiver) - extends CacheBasedSimsStore( - id = DBV2SimsRefreshStore.Identifier, - fetcher = newSimsRefreshOnUserClientColumn.fetcher, - maxCacheSize = DBV2SimsRefreshStore.MaxCacheSize, - cacheTtl = DBV2SimsRefreshStore.CacheTTL, - statsReceiver = statsReceiver.scope("CachedDBV2SimsRefreshStore", "cache") - ) - -object DBV2SimsRefreshStore { - val Identifier = CandidateSourceIdentifier(Algorithm.Sims.toString) - val MaxCacheSize = 5000 - val CacheTTL: Duration = Duration.fromHours(24) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/DBV2SimsStore.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/DBV2SimsStore.scala deleted file mode 100644 index ae291eddc..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/DBV2SimsStore.scala +++ /dev/null @@ -1,38 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims - -import com.google.inject.Singleton -import com.google.inject.name.Named -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.hermit.candidate.thriftscala.Candidates -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.client.Fetcher -import com.twitter.util.Duration - -import javax.inject.Inject - -@Singleton -class DBV2SimsStore @Inject() ( - @Named(GuiceNamedConstants.DBV2_SIMS_FETCHER) fetcher: Fetcher[Long, Unit, Candidates]) - extends StratoBasedSimsCandidateSourceWithUnitView( - fetcher, - identifier = DBV2SimsStore.Identifier) - -@Singleton -class CachedDBV2SimsStore @Inject() ( - @Named(GuiceNamedConstants.DBV2_SIMS_FETCHER) fetcher: Fetcher[Long, Unit, Candidates], - statsReceiver: StatsReceiver) - extends CacheBasedSimsStore( - id = DBV2SimsStore.Identifier, - fetcher = fetcher, - maxCacheSize = DBV2SimsStore.MaxCacheSize, - cacheTtl = DBV2SimsStore.CacheTTL, - statsReceiver = statsReceiver.scope("CachedDBV2SimsStore", "cache") - ) - -object DBV2SimsStore { - val Identifier = CandidateSourceIdentifier(Algorithm.Sims.toString) - val MaxCacheSize = 1000 - val CacheTTL: Duration = Duration.fromHours(24) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/Follow2vecNearestNeighborsStore.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/Follow2vecNearestNeighborsStore.scala deleted file mode 100644 index 14131ffd3..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/Follow2vecNearestNeighborsStore.scala +++ /dev/null @@ -1,69 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims - -import com.google.inject.Singleton -import com.twitter.follow_recommendations.common.candidate_sources.sims.Follow2vecNearestNeighborsStore.NearestNeighborParamsType -import com.twitter.hermit.candidate.thriftscala.Candidate -import com.twitter.hermit.candidate.thriftscala.Candidates -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import com.twitter.strato.catalog.Fetch -import com.twitter.strato.client.Fetcher -import com.twitter.strato.generated.client.recommendations.follow2vec.LinearRegressionFollow2vecNearestNeighborsClientColumn -import com.twitter.util.Return -import com.twitter.util.Throw -import javax.inject.Inject - -@Singleton -class LinearRegressionFollow2vecNearestNeighborsStore @Inject() ( - linearRegressionFollow2vecNearestNeighborsClientColumn: LinearRegressionFollow2vecNearestNeighborsClientColumn) - extends StratoBasedSimsCandidateSource[NearestNeighborParamsType]( - Follow2vecNearestNeighborsStore.convertFetcher( - linearRegressionFollow2vecNearestNeighborsClientColumn.fetcher), - view = Follow2vecNearestNeighborsStore.defaultSearchParams, - identifier = Follow2vecNearestNeighborsStore.IdentifierF2vLinearRegression - ) - -object Follow2vecNearestNeighborsStore { - // (userid, feature store version for data) - type NearestNeighborKeyType = (Long, Long) - // (neighbors to be returned, ef value: accuracy / latency tradeoff, distance for filtering) - type NearestNeighborParamsType = (Option[Int], Option[Int], Option[Double]) - // (seq(found neighbor id, score), distance for filtering) - type NearestNeighborValueType = (Seq[(Long, Option[Double])], Option[Double]) - - val IdentifierF2vLinearRegression: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.LinearRegressionFollow2VecNearestNeighbors.toString) - - val defaultFeatureStoreVersion: Long = 20210708 - val defaultSearchParams: NearestNeighborParamsType = (None, None, None) - - def convertFetcher( - fetcher: Fetcher[NearestNeighborKeyType, NearestNeighborParamsType, NearestNeighborValueType] - ): Fetcher[Long, NearestNeighborParamsType, Candidates] = { - (key: Long, view: NearestNeighborParamsType) => - { - def toCandidates( - results: Option[NearestNeighborValueType] - ): Option[Candidates] = { - results.flatMap { r => - Some( - Candidates( - key, - r._1.map { neighbor => - Candidate(neighbor._1, neighbor._2.getOrElse(0)) - } - ) - ) - } - } - - val results: Stitch[Fetch.Result[NearestNeighborValueType]] = - fetcher.fetch(key = (key, defaultFeatureStoreVersion), view = view) - results.transform { - case Return(r) => Stitch.value(Fetch.Result(toCandidates(r.v))) - case Throw(e) => Stitch.exception(e) - } - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/README.md deleted file mode 100644 index 97bab500a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/README.md +++ /dev/null @@ -1,32 +0,0 @@ -# Sims Candidate Source -Offers various online sources for finding similar accounts based on a given user, whether it is the target user or an account candidate. - -## Sims -The objective is to identify a list of K users who are similar to a given user. In this scenario, we primarily focus on finding similar users as "producers" rather than "consumers." Sims has two steps: candidate generation and ranking. - -### Sims Candidate Generation - -With over 700 million users to consider, there are multiple ways to define similarities. Currently, we have three candidate sources for Sims: - -**CosineFollow** (based on user-user follow graph): The similarity between two users is defined as the cosine similarity between their followers. Despite sounding simple, computing all-pair similarity on the entire follow graph is computationally challenging. We are currently using the WHIMP algorithm to find the top 1000 similar users for each user ID. This candidate source has the largest coverage, as it can find similar user candidates for more than 700 million users. - -**CosineList** (based on user-list membership graph): The similarity between two users is defined as the cosine similarity between the lists they are included as members (e.g., [here](https://twitter.com/jack/lists/memberships) are the lists that @jack is on). The same algorithm as CosineFollow is used. - -**Follow2Vec** (essentially Word2Vec on user-user follow graph): We first train the Word2Vec model on follow sequence data to obtain users' embeddings and then find the most similar users based on the similarity of the embeddings. However, we need enough data for each user to learn a meaningful embedding for them, so we can only obtain embeddings for the top 10 million users (currently in production, testing 30 million users). Furthermore, Word2Vec model training is limited by memory and computation as it is trained on a single machine. - -##### Cosine Similarity -A crucial component in Sims is calculating cosine similarities between users based on a user-X (X can be a user, list, or other entities) bipartite graph. This problem is technically challenging and took several years of effort to solve. - -The current implementation uses the algorithm proposed in [When hashes met wedges: A distributed algorithm for finding high similarity vectors. WWW 2017](https://arxiv.org/pdf/1703.01054.pdf) - -### Sims Ranking -After the candidate generation step, we can obtain dozens to hundreds of similar user candidates for each user. However, since these candidates come from different algorithms, we need a way to rank them. To do this, we collect user feedback. - -We use the "Profile Sidebar Impressions & Follow" (a module with follow suggestions displayed when a user visits a profile page and scrolls down) to collect training data. To alleviate any system bias, we use 4% of traffic to show randomly shuffled candidates to users and collect positive (followed impression) and negative (impression only) data from this traffic. This data is used as an evaluation set. We use a portion of the remaining 96% of traffic for training data, filtering only for sets of impressions that had at least one follow, ensuring that the user taking action was paying attention to the impressions. - -The examples are in the format of (profile_user, candidate_user, label). We add features for profile_users and candidate_users based on some high-level aggregated statistics in a feature dataset provided by the Customer Journey team, as well as features that represent the similarity between the profile_user and candidate_user. - -We employ a multi-tower MLP model and optimize the logistic loss. The model is refreshed weekly using an ML workflow. - -We recompute the candidates and rank them daily. The ranked results are published to the Manhattan dataset. - diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsExperimentalStore.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsExperimentalStore.scala deleted file mode 100644 index d144640c7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsExperimentalStore.scala +++ /dev/null @@ -1,36 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims - -import com.google.inject.Singleton -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.generated.client.recommendations.similarity.SimilarUsersBySimsExperimentalOnUserClientColumn -import com.twitter.util.Duration - -import javax.inject.Inject - -@Singleton -class SimsExperimentalStore @Inject() ( - simsExperimentalOnUserClientColumn: SimilarUsersBySimsExperimentalOnUserClientColumn) - extends StratoBasedSimsCandidateSourceWithUnitView( - fetcher = simsExperimentalOnUserClientColumn.fetcher, - identifier = SimsExperimentalStore.Identifier - ) - -@Singleton -class CachedSimsExperimentalStore @Inject() ( - simsExperimentalOnUserClientColumn: SimilarUsersBySimsExperimentalOnUserClientColumn, - statsReceiver: StatsReceiver) - extends CacheBasedSimsStore( - id = SimsExperimentalStore.Identifier, - fetcher = simsExperimentalOnUserClientColumn.fetcher, - maxCacheSize = SimsExperimentalStore.MaxCacheSize, - cacheTtl = SimsExperimentalStore.CacheTTL, - statsReceiver = statsReceiver.scope("CachedSimsExperimentalStore", "cache") - ) - -object SimsExperimentalStore { - val Identifier = CandidateSourceIdentifier(Algorithm.Sims.toString) - val MaxCacheSize = 1000 - val CacheTTL: Duration = Duration.fromHours(12) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsSourceFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsSourceFSConfig.scala deleted file mode 100644 index 0bacb339f..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsSourceFSConfig.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.FSParam -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class SimsSourceFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[FSParam[Boolean] with FSName] = Seq( - SimsSourceParams.DisableHeavyRanker - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsSourceParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsSourceParams.scala deleted file mode 100644 index e96775a6a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsSourceParams.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims - -import com.twitter.timelines.configapi.FSParam - -object SimsSourceParams { - case object EnableDBV2SimsStore extends FSParam[Boolean]("sims_source_enable_dbv2_source", false) - - case object EnableDBV2SimsRefreshStore - extends FSParam[Boolean]("sims_source_enable_dbv2_refresh_source", false) - - case object EnableExperimentalSimsStore - extends FSParam[Boolean]("sims_source_enable_experimental_source", false) - - case object DisableHeavyRanker - extends FSParam[Boolean]("sims_source_disable_heavy_ranker", default = false) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsStore.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsStore.scala deleted file mode 100644 index 98a00a9a4..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SimsStore.scala +++ /dev/null @@ -1,36 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims - -import com.google.inject.Singleton -import com.google.inject.name.Named -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.hermit.candidate.thriftscala.Candidates -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.client.Fetcher -import com.twitter.util.Duration - -import javax.inject.Inject - -@Singleton -class SimsStore @Inject() ( - @Named(GuiceNamedConstants.SIMS_FETCHER) fetcher: Fetcher[Long, Unit, Candidates]) - extends StratoBasedSimsCandidateSourceWithUnitView(fetcher, identifier = SimsStore.Identifier) - -@Singleton -class CachedSimsStore @Inject() ( - @Named(GuiceNamedConstants.SIMS_FETCHER) fetcher: Fetcher[Long, Unit, Candidates], - statsReceiver: StatsReceiver) - extends CacheBasedSimsStore( - id = SimsStore.Identifier, - fetcher = fetcher, - maxCacheSize = SimsStore.MaxCacheSize, - cacheTtl = SimsStore.CacheTTL, - statsReceiver = statsReceiver.scope("CachedSimsStore", "cache") - ) - -object SimsStore { - val Identifier = CandidateSourceIdentifier(Algorithm.Sims.toString) - val MaxCacheSize = 50000 - val CacheTTL: Duration = Duration.fromHours(24) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/StratoBasedSimsCandidateSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/StratoBasedSimsCandidateSource.scala deleted file mode 100644 index 6d862c849..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/StratoBasedSimsCandidateSource.scala +++ /dev/null @@ -1,40 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims - -import com.twitter.follow_recommendations.common.candidate_sources.base.StratoFetcherSource -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.follow_recommendations.common.models.SimilarToProof -import com.twitter.hermit.candidate.thriftscala.Candidates -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.client.Fetcher - -abstract class StratoBasedSimsCandidateSource[U]( - fetcher: Fetcher[Long, U, Candidates], - view: U, - override val identifier: CandidateSourceIdentifier) - extends StratoFetcherSource[Long, U, Candidates](fetcher, view, identifier) { - - override def map(target: Long, candidates: Candidates): Seq[CandidateUser] = - StratoBasedSimsCandidateSource.map(target, candidates) -} - -object StratoBasedSimsCandidateSource { - def map(target: Long, candidates: Candidates): Seq[CandidateUser] = { - for { - candidate <- candidates.candidates - } yield CandidateUser( - id = candidate.userId, - score = Some(candidate.score), - reason = Some( - Reason( - Some( - AccountProof( - similarToProof = Some(SimilarToProof(Seq(target))) - ) - ) - ) - ) - ) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/StratoBasedSimsCandidateSourceWithUnitView.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/StratoBasedSimsCandidateSourceWithUnitView.scala deleted file mode 100644 index af1133893..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/StratoBasedSimsCandidateSourceWithUnitView.scala +++ /dev/null @@ -1,10 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims - -import com.twitter.hermit.candidate.thriftscala.Candidates -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.client.Fetcher - -abstract class StratoBasedSimsCandidateSourceWithUnitView( - fetcher: Fetcher[Long, Unit, Candidates], - override val identifier: CandidateSourceIdentifier) - extends StratoBasedSimsCandidateSource[Unit](fetcher, Unit, identifier) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SwitchingSimsSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SwitchingSimsSource.scala deleted file mode 100644 index 0d297a806..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/SwitchingSimsSource.scala +++ /dev/null @@ -1,55 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims - -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class SwitchingSimsSource @Inject() ( - cachedDBV2SimsStore: CachedDBV2SimsStore, - cachedDBV2SimsRefreshStore: CachedDBV2SimsRefreshStore, - cachedSimsExperimentalStore: CachedSimsExperimentalStore, - cachedSimsStore: CachedSimsStore, - statsReceiver: StatsReceiver = NullStatsReceiver) - extends CandidateSource[HasParams with HasSimilarToContext, CandidateUser] { - - override val identifier: CandidateSourceIdentifier = SwitchingSimsSource.Identifier - - private val stats = statsReceiver.scope("SwitchingSimsSource") - private val dbV2SimsStoreCounter = stats.counter("DBV2SimsStore") - private val dbV2SimsRefreshStoreCounter = stats.counter("DBV2SimsRefreshStore") - private val simsExperimentalStoreCounter = stats.counter("SimsExperimentalStore") - private val simsStoreCounter = stats.counter("SimsStore") - - override def apply(request: HasParams with HasSimilarToContext): Stitch[Seq[CandidateUser]] = { - val selectedSimsStore = - if (request.params(SimsSourceParams.EnableDBV2SimsStore)) { - dbV2SimsStoreCounter.incr() - cachedDBV2SimsStore - } else if (request.params(SimsSourceParams.EnableDBV2SimsRefreshStore)) { - dbV2SimsRefreshStoreCounter.incr() - cachedDBV2SimsRefreshStore - } else if (request.params(SimsSourceParams.EnableExperimentalSimsStore)) { - simsExperimentalStoreCounter.incr() - cachedSimsExperimentalStore - } else { - simsStoreCounter.incr() - cachedSimsStore - } - stats.counter("total").incr() - selectedSimsStore(request) - } -} - -object SwitchingSimsSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier(Algorithm.Sims.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/BUILD deleted file mode 100644 index f5ccb66e7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/BUILD +++ /dev/null @@ -1,23 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/DBV2SimsExpansionParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/DBV2SimsExpansionParams.scala deleted file mode 100644 index c323ad1f3..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/DBV2SimsExpansionParams.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims_expansion - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam - -object DBV2SimsExpansionParams { - // Theses divisors are used to calibrate DBv2Sims extension candidates scores - case object RecentFollowingSimilarUsersDBV2CalibrateDivisor - extends FSBoundedParam[Double]( - "sims_expansion_recent_following_similar_users_dbv2_divisor", - default = 1.0d, - min = 0.1d, - max = 100d) - case object RecentEngagementSimilarUsersDBV2CalibrateDivisor - extends FSBoundedParam[Double]( - "sims_expansion_recent_engagement_similar_users_dbv2_divisor", - default = 1.0d, - min = 0.1d, - max = 100d) - case object DisableHeavyRanker - extends FSParam[Boolean]("sims_expansion_disable_heavy_ranker", default = false) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/README.md deleted file mode 100644 index 6143d5868..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Sims Expansion Candidate Source -provides similar accounts based on the Sims algorithm for a given set of accounts. - -This is a 2nd-hop expansion, meaning that the input accounts could be a user's recently engaged, followed, or algorithm-generated (such as RealGraph) accounts. - -For more information on Sims and how it is utilized in the Follow Recommendations Service, please refer to the `follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims/README.md` file. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersFSConfig.scala deleted file mode 100644 index 5642f5852..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersFSConfig.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims_expansion - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSParam - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RecentEngagementSimilarUsersFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[FSParam[Boolean]] = Seq( - RecentEngagementSimilarUsersParams.FirstDegreeSortEnabled - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersParams.scala deleted file mode 100644 index 4b1729702..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersParams.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims_expansion - -import com.twitter.timelines.configapi.FSEnumParam -import com.twitter.timelines.configapi.FSParam - -object RecentEngagementSimilarUsersParams { - - case object FirstDegreeSortEnabled - extends FSParam[Boolean]( - name = "sims_expansion_recent_engagement_first_degree_sort", - default = true) - case object Aggregator - extends FSEnumParam[SimsExpansionSourceAggregatorId.type]( - name = "sims_expansion_recent_engagement_aggregator_id", - default = SimsExpansionSourceAggregatorId.Sum, - enum = SimsExpansionSourceAggregatorId) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersSource.scala deleted file mode 100644 index 0d99c2dc2..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentEngagementSimilarUsersSource.scala +++ /dev/null @@ -1,113 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims_expansion - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.candidate_sources.sims.SwitchingSimsSource -import com.twitter.follow_recommendations.common.clients.real_time_real_graph.RealTimeRealGraphClient -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.follow_recommendations.common.models.SimilarToProof -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RecentEngagementSimilarUsersSource @Inject() ( - realTimeRealGraphClient: RealTimeRealGraphClient, - switchingSimsSource: SwitchingSimsSource, - statsReceiver: StatsReceiver) - extends SimsExpansionBasedCandidateSource[HasClientContext with HasParams]( - switchingSimsSource) { - override def maxSecondaryDegreeNodes(req: HasClientContext with HasParams): Int = Int.MaxValue - - override def maxResults(req: HasClientContext with HasParams): Int = - RecentEngagementSimilarUsersSource.MaxResults - - override val identifier: CandidateSourceIdentifier = RecentEngagementSimilarUsersSource.Identifier - private val stats = statsReceiver.scope(identifier.name) - private val calibratedScoreCounter = stats.counter("calibrated_scores_counter") - - override def scoreCandidate(sourceScore: Double, similarToScore: Double): Double = { - sourceScore * similarToScore - } - - override def calibrateDivisor(req: HasClientContext with HasParams): Double = { - req.params(DBV2SimsExpansionParams.RecentEngagementSimilarUsersDBV2CalibrateDivisor) - } - - override def calibrateScore( - candidateScore: Double, - req: HasClientContext with HasParams - ): Double = { - calibratedScoreCounter.incr() - candidateScore / calibrateDivisor(req) - } - - /** - * fetch first degree nodes given request - */ - override def firstDegreeNodes( - target: HasClientContext with HasParams - ): Stitch[Seq[CandidateUser]] = { - target.getOptionalUserId - .map { userId => - realTimeRealGraphClient - .getUsersRecentlyEngagedWith( - userId, - RealTimeRealGraphClient.EngagementScoreMap, - includeDirectFollowCandidates = true, - includeNonDirectFollowCandidates = true - ).map(_.sortBy(-_.score.getOrElse(0.0d)) - .take(RecentEngagementSimilarUsersSource.MaxFirstDegreeNodes)) - }.getOrElse(Stitch.Nil) - } - - override def aggregateAndScore( - request: HasClientContext with HasParams, - firstDegreeToSecondDegreeNodesMap: Map[CandidateUser, Seq[SimilarUser]] - ): Stitch[Seq[CandidateUser]] = { - - val inputNodes = firstDegreeToSecondDegreeNodesMap.keys.map(_.id).toSet - val aggregator = request.params(RecentEngagementSimilarUsersParams.Aggregator) match { - case SimsExpansionSourceAggregatorId.Max => - SimsExpansionBasedCandidateSource.ScoreAggregator.Max - case SimsExpansionSourceAggregatorId.Sum => - SimsExpansionBasedCandidateSource.ScoreAggregator.Sum - case SimsExpansionSourceAggregatorId.MultiDecay => - SimsExpansionBasedCandidateSource.ScoreAggregator.MultiDecay - } - - val groupedCandidates = firstDegreeToSecondDegreeNodesMap.values.flatten - .filterNot(c => inputNodes.contains(c.candidateId)) - .groupBy(_.candidateId) - .map { - case (id, candidates) => - // Different aggregators for final score - val finalScore = aggregator(candidates.map(_.score).toSeq) - val proofs = candidates.map(_.similarTo).toSet - - CandidateUser( - id = id, - score = Some(finalScore), - reason = - Some(Reason(Some(AccountProof(similarToProof = Some(SimilarToProof(proofs.toSeq)))))) - ).withCandidateSource(identifier) - } - .toSeq - .sortBy(-_.score.getOrElse(0.0d)) - .take(maxResults(request)) - - Stitch.value(groupedCandidates) - } -} - -object RecentEngagementSimilarUsersSource { - val Identifier = CandidateSourceIdentifier(Algorithm.RecentEngagementSimilarUser.toString) - val MaxFirstDegreeNodes = 10 - val MaxResults = 200 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentFollowingSimilarUsersParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentFollowingSimilarUsersParams.scala deleted file mode 100644 index 44b1378d4..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentFollowingSimilarUsersParams.scala +++ /dev/null @@ -1,29 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims_expansion - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam - -object RecentFollowingSimilarUsersParams { - case object MaxFirstDegreeNodes - extends FSBoundedParam[Int]( - name = "sims_expansion_recent_following_max_first_degree_nodes", - default = 10, - min = 0, - max = 200) - case object MaxSecondaryDegreeExpansionPerNode - extends FSBoundedParam[Int]( - name = "sims_expansion_recent_following_max_secondary_degree_nodes", - default = 40, - min = 0, - max = 200) - case object MaxResults - extends FSBoundedParam[Int]( - name = "sims_expansion_recent_following_max_results", - default = 200, - min = 0, - max = 200) - case object TimestampIntegrated - extends FSParam[Boolean]( - name = "sims_expansion_recent_following_integ_timestamp", - default = false) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentFollowingSimilarUsersSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentFollowingSimilarUsersSource.scala deleted file mode 100644 index b5a187fc5..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentFollowingSimilarUsersSource.scala +++ /dev/null @@ -1,99 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims_expansion - -import com.google.inject.Singleton -import com.twitter.follow_recommendations.common.candidate_sources.sims.SwitchingSimsSource -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.clients.socialgraph.SocialGraphClient -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import javax.inject.Inject - -object RecentFollowingSimilarUsersSource { - - val Identifier = CandidateSourceIdentifier(Algorithm.NewFollowingSimilarUser.toString) -} - -@Singleton -class RecentFollowingSimilarUsersSource @Inject() ( - socialGraph: SocialGraphClient, - switchingSimsSource: SwitchingSimsSource, - statsReceiver: StatsReceiver) - extends SimsExpansionBasedCandidateSource[ - HasParams with HasRecentFollowedUserIds with HasClientContext - ](switchingSimsSource) { - - val identifier = RecentFollowingSimilarUsersSource.Identifier - private val stats = statsReceiver.scope(identifier.name) - private val maxResultsStats = stats.scope("max_results") - private val calibratedScoreCounter = stats.counter("calibrated_scores_counter") - - override def firstDegreeNodes( - request: HasParams with HasRecentFollowedUserIds with HasClientContext - ): Stitch[Seq[CandidateUser]] = { - if (request.params(RecentFollowingSimilarUsersParams.TimestampIntegrated)) { - val recentFollowedUserIdsWithTimeStitch = - socialGraph.getRecentFollowedUserIdsWithTime(request.clientContext.userId.get) - - recentFollowedUserIdsWithTimeStitch.map { results => - val first_degree_nodes = results - .sortBy(-_.timeInMs).take( - request.params(RecentFollowingSimilarUsersParams.MaxFirstDegreeNodes)) - val max_timestamp = first_degree_nodes.head.timeInMs - first_degree_nodes.map { - case userIdWithTime => - CandidateUser( - userIdWithTime.userId, - score = Some(userIdWithTime.timeInMs.toDouble / max_timestamp)) - } - } - } else { - Stitch.value( - request.recentFollowedUserIds - .getOrElse(Nil).take( - request.params(RecentFollowingSimilarUsersParams.MaxFirstDegreeNodes)).map( - CandidateUser(_, score = Some(1.0))) - ) - } - } - - override def maxSecondaryDegreeNodes( - req: HasParams with HasRecentFollowedUserIds with HasClientContext - ): Int = { - req.params(RecentFollowingSimilarUsersParams.MaxSecondaryDegreeExpansionPerNode) - } - - override def maxResults( - req: HasParams with HasRecentFollowedUserIds with HasClientContext - ): Int = { - val firstDegreeNodes = req.params(RecentFollowingSimilarUsersParams.MaxFirstDegreeNodes) - val maxResultsNum = req.params(RecentFollowingSimilarUsersParams.MaxResults) - maxResultsStats - .stat( - s"RecentFollowingSimilarUsersSource_firstDegreeNodes_${firstDegreeNodes}_maxResults_${maxResultsNum}") - .add(1) - maxResultsNum - } - - override def scoreCandidate(sourceScore: Double, similarToScore: Double): Double = { - sourceScore * similarToScore - } - - override def calibrateDivisor( - req: HasParams with HasRecentFollowedUserIds with HasClientContext - ): Double = { - req.params(DBV2SimsExpansionParams.RecentFollowingSimilarUsersDBV2CalibrateDivisor) - } - - override def calibrateScore( - candidateScore: Double, - req: HasParams with HasRecentFollowedUserIds with HasClientContext - ): Double = { - calibratedScoreCounter.incr() - candidateScore / calibrateDivisor(req) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentStrongEngagementDirectFollowSimilarUsersSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentStrongEngagementDirectFollowSimilarUsersSource.scala deleted file mode 100644 index 0898aabfb..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/RecentStrongEngagementDirectFollowSimilarUsersSource.scala +++ /dev/null @@ -1,53 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims_expansion - -import com.google.inject.Singleton -import com.twitter.follow_recommendations.common.candidate_sources.sims.SwitchingSimsSource -import com.twitter.follow_recommendations.common.clients.real_time_real_graph.RealTimeRealGraphClient -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -import javax.inject.Inject - -@Singleton -class RecentStrongEngagementDirectFollowSimilarUsersSource @Inject() ( - realTimeRealGraphClient: RealTimeRealGraphClient, - switchingSimsSource: SwitchingSimsSource) - extends SimsExpansionBasedCandidateSource[HasClientContext with HasParams]( - switchingSimsSource) { - - val identifier = RecentStrongEngagementDirectFollowSimilarUsersSource.Identifier - - override def firstDegreeNodes( - request: HasClientContext with HasParams - ): Stitch[Seq[CandidateUser]] = request.getOptionalUserId - .map { userId => - realTimeRealGraphClient - .getUsersRecentlyEngagedWith( - userId, - RealTimeRealGraphClient.StrongEngagementScoreMap, - includeDirectFollowCandidates = true, - includeNonDirectFollowCandidates = false - ).map(_.take(RecentStrongEngagementDirectFollowSimilarUsersSource.MaxFirstDegreeNodes)) - }.getOrElse(Stitch.Nil) - - override def maxSecondaryDegreeNodes(request: HasClientContext with HasParams): Int = Int.MaxValue - - override def maxResults(request: HasClientContext with HasParams): Int = - RecentStrongEngagementDirectFollowSimilarUsersSource.MaxResults - - override def scoreCandidate(sourceScore: Double, similarToScore: Double): Double = { - sourceScore * similarToScore - } - - override def calibrateDivisor(req: HasClientContext with HasParams): Double = 1.0d -} - -object RecentStrongEngagementDirectFollowSimilarUsersSource { - val Identifier = CandidateSourceIdentifier(Algorithm.RecentStrongEngagementSimilarUser.toString) - val MaxFirstDegreeNodes = 10 - val MaxResults = 200 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionBasedCandidateSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionBasedCandidateSource.scala deleted file mode 100644 index 018fe413b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionBasedCandidateSource.scala +++ /dev/null @@ -1,114 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims_expansion - -import com.twitter.follow_recommendations.common.candidate_sources.base.TwoHopExpansionCandidateSource -import com.twitter.follow_recommendations.common.candidate_sources.sims.SwitchingSimsSource -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.follow_recommendations.common.models.SimilarToProof -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import scala.math._ - -case class SimilarUser(candidateId: Long, similarTo: Long, score: Double) - -abstract class SimsExpansionBasedCandidateSource[-Target <: HasParams]( - switchingSimsSource: SwitchingSimsSource) - extends TwoHopExpansionCandidateSource[Target, CandidateUser, SimilarUser, CandidateUser] { - - // max number secondary degree nodes per first degree node - def maxSecondaryDegreeNodes(req: Target): Int - - // max number output results - def maxResults(req: Target): Int - - // scorer to score candidate based on first and second degree node scores - def scoreCandidate(source: Double, similarToScore: Double): Double - - def calibrateDivisor(req: Target): Double - - def calibrateScore(candidateScore: Double, req: Target): Double = { - candidateScore / calibrateDivisor(req) - } - - override def secondaryDegreeNodes(req: Target, node: CandidateUser): Stitch[Seq[SimilarUser]] = { - switchingSimsSource(new HasParams with HasSimilarToContext { - override val similarToUserIds = Seq(node.id) - override val params = (req.params) - }).map(_.take(maxSecondaryDegreeNodes(req)).map { candidate => - SimilarUser( - candidate.id, - node.id, - (node.score, candidate.score) match { - // only calibrated sims expanded candidates scores - case (Some(nodeScore), Some(candidateScore)) => - calibrateScore(scoreCandidate(nodeScore, candidateScore), req) - case (Some(nodeScore), _) => nodeScore - // NewFollowingSimilarUser will enter this case - case _ => calibrateScore(candidate.score.getOrElse(0.0), req) - } - ) - }) - } - - override def aggregateAndScore( - request: Target, - firstDegreeToSecondDegreeNodesMap: Map[CandidateUser, Seq[SimilarUser]] - ): Stitch[Seq[CandidateUser]] = { - - val inputNodes = firstDegreeToSecondDegreeNodesMap.keys.map(_.id).toSet - val aggregator = request.params(SimsExpansionSourceParams.Aggregator) match { - case SimsExpansionSourceAggregatorId.Max => - SimsExpansionBasedCandidateSource.ScoreAggregator.Max - case SimsExpansionSourceAggregatorId.Sum => - SimsExpansionBasedCandidateSource.ScoreAggregator.Sum - case SimsExpansionSourceAggregatorId.MultiDecay => - SimsExpansionBasedCandidateSource.ScoreAggregator.MultiDecay - } - - val groupedCandidates = firstDegreeToSecondDegreeNodesMap.values.flatten - .filterNot(c => inputNodes.contains(c.candidateId)) - .groupBy(_.candidateId) - .map { - case (id, candidates) => - // Different aggregators for final score - val finalScore = aggregator(candidates.map(_.score).toSeq) - val proofs = candidates.map(_.similarTo).toSet - - CandidateUser( - id = id, - score = Some(finalScore), - reason = - Some(Reason(Some(AccountProof(similarToProof = Some(SimilarToProof(proofs.toSeq)))))) - ).withCandidateSource(identifier) - } - .toSeq - .sortBy(-_.score.getOrElse(0.0d)) - .take(maxResults(request)) - - Stitch.value(groupedCandidates) - } -} - -object SimsExpansionBasedCandidateSource { - object ScoreAggregator { - val Max: Seq[Double] => Double = (candidateScores: Seq[Double]) => { - if (candidateScores.size > 0) candidateScores.max else 0.0 - } - val Sum: Seq[Double] => Double = (candidateScores: Seq[Double]) => { - candidateScores.sum - } - val MultiDecay: Seq[Double] => Double = (candidateScores: Seq[Double]) => { - val alpha = 0.1 - val beta = 0.1 - val gamma = 0.8 - val decay_scores: Seq[Double] = - candidateScores - .sorted(Ordering[Double].reverse) - .zipWithIndex - .map(x => x._1 * pow(gamma, x._2)) - alpha * candidateScores.max + decay_scores.sum + beta * candidateScores.size - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionFSConfig.scala deleted file mode 100644 index b145ee607..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionFSConfig.scala +++ /dev/null @@ -1,26 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims_expansion - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class SimsExpansionFSConfig @Inject() () extends FeatureSwitchConfig { - override val intFSParams: Seq[FSBoundedParam[Int]] = Seq( - RecentFollowingSimilarUsersParams.MaxFirstDegreeNodes, - RecentFollowingSimilarUsersParams.MaxSecondaryDegreeExpansionPerNode, - RecentFollowingSimilarUsersParams.MaxResults - ) - - override val doubleFSParams: Seq[FSBoundedParam[Double]] = Seq( - DBV2SimsExpansionParams.RecentFollowingSimilarUsersDBV2CalibrateDivisor, - DBV2SimsExpansionParams.RecentEngagementSimilarUsersDBV2CalibrateDivisor - ) - - override val booleanFSParams: Seq[FSParam[Boolean]] = Seq( - DBV2SimsExpansionParams.DisableHeavyRanker, - RecentFollowingSimilarUsersParams.TimestampIntegrated - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionSourceParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionSourceParams.scala deleted file mode 100644 index f03ccceea..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion/SimsExpansionSourceParams.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.sims_expansion -import com.twitter.timelines.configapi.FSEnumParam - -object SimsExpansionSourceParams { - case object Aggregator - extends FSEnumParam[SimsExpansionSourceAggregatorId.type]( - name = "sims_expansion_aggregator_id", - default = SimsExpansionSourceAggregatorId.Sum, - enum = SimsExpansionSourceAggregatorId) -} - -object SimsExpansionSourceAggregatorId extends Enumeration { - type AggregatorId = Value - val Sum: AggregatorId = Value("sum") - val Max: AggregatorId = Value("max") - val MultiDecay: AggregatorId = Value("multi_decay") -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/BUILD deleted file mode 100644 index e0392df45..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/BUILD +++ /dev/null @@ -1,18 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/README.md deleted file mode 100644 index bfdbacd25..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/README.md +++ /dev/null @@ -1,6 +0,0 @@ -# Social Graph Candidate Source -Provides candidate expansion based on the Twitter social graph. - -Currently, the expansion is mainly based on the "follow" social graph edge, which allows the service to identify recent following accounts for a given set of accounts. The input accounts could be a user's recent following, engaged, or other related accounts. - -In summary, the Social Graph Candidate Source utilizes the Twitter social graph to provide candidate expansion, primarily focusing on recent following accounts. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSource.scala deleted file mode 100644 index 577213cc1..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSource.scala +++ /dev/null @@ -1,102 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.socialgraph - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.candidate_sources.base.TwoHopExpansionCandidateSource -import com.twitter.follow_recommendations.common.clients.socialgraph.RecentEdgesQuery -import com.twitter.follow_recommendations.common.clients.socialgraph.SocialGraphClient -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FollowProof -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.hermit.model.Algorithm -import com.twitter.inject.Logging -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.socialgraph.thriftscala.RelationshipType -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject -import javax.inject.Singleton - -/** - * This candidate source is a two hop expansion over the follow graph. The candidates returned from this source is the users that get followed by the target user's recent followings. It will call SocialGraph `n` + 1 times where `n` is the number of recent followings of the target user to be considered. - */ -@Singleton -class RecentFollowingRecentFollowingExpansionSource @Inject() ( - socialGraphClient: SocialGraphClient, - statsReceiver: StatsReceiver) - extends TwoHopExpansionCandidateSource[ - HasParams with HasRecentFollowedUserIds, - Long, - Long, - CandidateUser - ] - with Logging { - - override val identifier: CandidateSourceIdentifier = - RecentFollowingRecentFollowingExpansionSource.Identifier - - val stats = statsReceiver.scope(identifier.name) - - override def firstDegreeNodes( - target: HasParams with HasRecentFollowedUserIds - ): Stitch[Seq[Long]] = Stitch.value( - target.recentFollowedUserIds - .getOrElse(Nil).take( - RecentFollowingRecentFollowingExpansionSource.NumFirstDegreeNodesToRetrieve) - ) - - override def secondaryDegreeNodes( - target: HasParams with HasRecentFollowedUserIds, - node: Long - ): Stitch[Seq[Long]] = socialGraphClient - .getRecentEdgesCached( - RecentEdgesQuery( - node, - Seq(RelationshipType.Following), - Some(RecentFollowingRecentFollowingExpansionSource.NumSecondDegreeNodesToRetrieve)), - useCachedStratoColumn = - target.params(RecentFollowingRecentFollowingExpansionSourceParams.CallSgsCachedColumn) - ).map( - _.take(RecentFollowingRecentFollowingExpansionSource.NumSecondDegreeNodesToRetrieve)).rescue { - case exception: Exception => - logger.warn( - s"${this.getClass} fails to retrieve second degree nodes for first degree node $node", - exception) - stats.counter("second_degree_expansion_error").incr() - Stitch.Nil - } - - override def aggregateAndScore( - target: HasParams with HasRecentFollowedUserIds, - firstDegreeToSecondDegreeNodesMap: Map[Long, Seq[Long]] - ): Stitch[Seq[CandidateUser]] = { - val zipped = firstDegreeToSecondDegreeNodesMap.toSeq.flatMap { - case (firstDegreeId, secondDegreeIds) => - secondDegreeIds.map(secondDegreeId => firstDegreeId -> secondDegreeId) - } - val candidateAndConnections = zipped - .groupBy { case (_, secondDegreeId) => secondDegreeId } - .mapValues { v => v.map { case (firstDegreeId, _) => firstDegreeId } } - .toSeq - .sortBy { case (_, connections) => -connections.size } - .map { - case (candidateId, connections) => - CandidateUser( - id = candidateId, - score = Some(CandidateUser.DefaultCandidateScore), - reason = Some( - Reason( - Some(AccountProof(followProof = Some(FollowProof(connections, connections.size)))))) - ).withCandidateSource(identifier) - } - Stitch.value(candidateAndConnections) - } -} - -object RecentFollowingRecentFollowingExpansionSource { - val Identifier = CandidateSourceIdentifier(Algorithm.NewFollowingNewFollowingExpansion.toString) - - val NumFirstDegreeNodesToRetrieve = 5 - val NumSecondDegreeNodesToRetrieve = 20 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSourceFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSourceFSConfig.scala deleted file mode 100644 index ac6403ebe..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSourceFSConfig.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.socialgraph - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.FSParam -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RecentFollowingRecentFollowingExpansionSourceFSConfig @Inject() () - extends FeatureSwitchConfig { - - override val booleanFSParams: Seq[FSParam[Boolean] with FSName] = Seq( - RecentFollowingRecentFollowingExpansionSourceParams.CallSgsCachedColumn, - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSourceParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSourceParams.scala deleted file mode 100644 index 4c51233ad..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph/RecentFollowingRecentFollowingExpansionSourceParams.scala +++ /dev/null @@ -1,10 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.socialgraph - -import com.twitter.timelines.configapi.FSParam - -object RecentFollowingRecentFollowingExpansionSourceParams { - object CallSgsCachedColumn - extends FSParam[Boolean]( - "recent_following_recent_following_source_call_sgs_cached_column", - true) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/BUILD deleted file mode 100644 index e89bade43..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/BUILD +++ /dev/null @@ -1,28 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/stores", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "src/scala/com/twitter/onboarding/relevance/features/strongtie", - "src/thrift/com/twitter/search/account_search/extended_network:extended_network_users-scala", - "strato/config/columns/hub:hub-strato-client", - "strato/config/columns/onboarding/userrecs:userrecs-strato-client", - "strato/config/columns/search/account_search:account_search-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/BaseOnlineSTPSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/BaseOnlineSTPSource.scala deleted file mode 100644 index 9f4365642..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/BaseOnlineSTPSource.scala +++ /dev/null @@ -1,55 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.follow_recommendations.common.models.STPGraph -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.logging.Logging -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.STPFeatureGenerator -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.STPRecord - -abstract class BaseOnlineSTPSource( - stpGraphBuilder: STPGraphBuilder, - baseStatsReceiver: StatsReceiver) - extends CandidateSource[ - HasClientContext with HasParams with HasRecentFollowedUserIds, - CandidateUser - ] - with Logging { - - protected val statsReceiver: StatsReceiver = baseStatsReceiver.scope("online_stp") - - override val identifier: CandidateSourceIdentifier = BaseOnlineSTPSource.Identifier - - def getCandidates( - records: Seq[STPRecord], - request: HasClientContext with HasParams with HasRecentFollowedUserIds - ): Stitch[Seq[CandidateUser]] - - override def apply( - request: HasClientContext with HasParams with HasRecentFollowedUserIds - ): Stitch[Seq[CandidateUser]] = - request.getOptionalUserId - .map { userId => - stpGraphBuilder(request) - .flatMap { graph: STPGraph => - logger.debug(graph) - val records = STPFeatureGenerator.constructFeatures( - userId, - graph.firstDegreeEdgeInfoList, - graph.secondDegreeEdgeInfoList) - getCandidates(records, request) - } - }.getOrElse(Stitch.Nil) -} - -object BaseOnlineSTPSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.OnlineStrongTiePredictionRecNoCaching.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/Dbv2StpScorer.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/Dbv2StpScorer.scala deleted file mode 100644 index 82308282a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/Dbv2StpScorer.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.cortex.deepbird.runtime.prediction_engine.TensorflowPredictionEngine -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.ml.api.Feature.Continuous -import com.twitter.ml.api.util.SRichDataRecord -import com.twitter.ml.prediction_service.PredictionRequest -import com.twitter.stitch.Stitch -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.STPRecord -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.STPRecordAdapter -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -/** - * STP ML ranker trained using DeepBirdV2 - */ -@Singleton -class Dbv2StpScorer @Inject() ( - @Named(GuiceNamedConstants.STP_DBV2_SCORER) tfPredictionEngine: TensorflowPredictionEngine) { - def getScoredResponse(record: STPRecord): Stitch[Option[Double]] = { - val request: PredictionRequest = new PredictionRequest( - STPRecordAdapter.adaptToDataRecord(record)) - val responseStitch = Stitch.callFuture(tfPredictionEngine.getPrediction(request)) - responseStitch.map { response => - val richDr = SRichDataRecord(response.getPrediction) - richDr.getFeatureValueOpt(new Continuous("output")) - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/EpStpScorer.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/EpStpScorer.scala deleted file mode 100644 index d02259575..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/EpStpScorer.scala +++ /dev/null @@ -1,65 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.bijection.scrooge.BinaryScalaCodec -import com.twitter.bijection.thrift.BinaryThriftCodec -import com.twitter.relevance.ep_model.scorer.EPScorer -import com.twitter.relevance.ep_model.scorer.ScorerUtil -import com.twitter.relevance.ep_model.thrift -import com.twitter.relevance.ep_model.thriftscala.EPScoringOptions -import com.twitter.relevance.ep_model.thriftscala.EPScoringRequest -import com.twitter.relevance.ep_model.thriftscala.EPScoringResponse -import com.twitter.relevance.ep_model.thriftscala.Record -import com.twitter.stitch.Stitch -import com.twitter.util.Future -import javax.inject.Inject -import javax.inject.Singleton -import scala.collection.JavaConverters._ -import scala.util.Success - -case class ScoredResponse(score: Double, featuresBreakdown: Option[String] = None) - -/** - * STP ML ranker trained using prehistoric ML framework - */ -@Singleton -class EpStpScorer @Inject() (epScorer: EPScorer) { - private def getScore(responses: List[EPScoringResponse]): Option[ScoredResponse] = - responses.headOption - .flatMap { response => - response.scores.flatMap { - _.headOption.map(score => ScoredResponse(ScorerUtil.normalize(score))) - } - } - - def getScoredResponse( - record: Record, - details: Boolean = false - ): Stitch[Option[ScoredResponse]] = { - val scoringOptions = EPScoringOptions( - addFeaturesBreakDown = details, - addTransformerIntermediateRecords = details - ) - val request = EPScoringRequest(auxFeatures = Some(Seq(record)), options = Some(scoringOptions)) - - Stitch.callFuture( - BinaryThriftCodec[thrift.EPScoringRequest] - .invert(BinaryScalaCodec(EPScoringRequest).apply(request)) - .map { thriftRequest: thrift.EPScoringRequest => - val responsesF = epScorer - .score(List(thriftRequest).asJava) - .map( - _.asScala.toList - .map(response => - BinaryScalaCodec(EPScoringResponse) - .invert(BinaryThriftCodec[thrift.EPScoringResponse].apply(response))) - .collect { case Success(response) => response } - ) - responsesF.map(getScore) - } - .getOrElse(Future(None))) - } -} - -object EpStpScorer { - val WithFeaturesBreakDown = false -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/MutualFollowStrongTiePredictionSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/MutualFollowStrongTiePredictionSource.scala deleted file mode 100644 index 10a269931..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/MutualFollowStrongTiePredictionSource.scala +++ /dev/null @@ -1,61 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.follow_recommendations.common.clients.socialgraph.RecentEdgesQuery -import com.twitter.follow_recommendations.common.clients.socialgraph.SocialGraphClient -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.socialgraph.thriftscala.RelationshipType -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.userrecs.StrongTiePredictionFeaturesOnUserClientColumn -import javax.inject.Singleton -import javax.inject.Inject - -/** - * Returns mutual follows. It first gets mutual follows from recent 100 follows and followers, and then unions this - * with mutual follows from STP features dataset. - */ -@Singleton -class MutualFollowStrongTiePredictionSource @Inject() ( - sgsClient: SocialGraphClient, - strongTiePredictionFeaturesOnUserClientColumn: StrongTiePredictionFeaturesOnUserClientColumn) - extends CandidateSource[HasClientContext with HasRecentFollowedUserIds, CandidateUser] { - val identifier: CandidateSourceIdentifier = - MutualFollowStrongTiePredictionSource.Identifier - - override def apply( - target: HasClientContext with HasRecentFollowedUserIds - ): Stitch[Seq[CandidateUser]] = { - target.getOptionalUserId match { - case Some(userId) => - val newFollowings = target.recentFollowedUserIds - .getOrElse(Nil) - .take(MutualFollowStrongTiePredictionSource.NumOfRecentFollowings) - val newFollowersStitch = - sgsClient - .getRecentEdges(RecentEdgesQuery(userId, Seq(RelationshipType.FollowedBy))).map( - _.take(MutualFollowStrongTiePredictionSource.NumOfRecentFollowers)) - val mutualFollowsStitch = - strongTiePredictionFeaturesOnUserClientColumn.fetcher - .fetch(userId).map(_.v.flatMap(_.topMutualFollows.map(_.map(_.userId))).getOrElse(Nil)) - - Stitch.join(newFollowersStitch, mutualFollowsStitch).map { - case (newFollowers, mutualFollows) => { - (newFollowings.intersect(newFollowers) ++ mutualFollows).distinct - .map(id => CandidateUser(id, Some(CandidateUser.DefaultCandidateScore))) - } - } - case _ => Stitch.Nil - } - } -} - -object MutualFollowStrongTiePredictionSource { - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.MutualFollowSTP.toString) - val NumOfRecentFollowings = 100 - val NumOfRecentFollowers = 100 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineMutualFollowExpansionSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineMutualFollowExpansionSource.scala deleted file mode 100644 index 37981f843..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineMutualFollowExpansionSource.scala +++ /dev/null @@ -1,23 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.google.inject.Singleton -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.generated.client.onboarding.userrecs.MutualFollowExpansionClientColumn -import javax.inject.Inject - -/** - * A source that finds the mutual follows of one's mutual follows that one isn't following already. - */ -@Singleton -class OfflineMutualFollowExpansionSource @Inject() ( - column: MutualFollowExpansionClientColumn) - extends OfflineStrongTiePredictionBaseSource(column.fetcher) { - override val identifier: CandidateSourceIdentifier = - OfflineMutualFollowExpansionSource.Identifier -} - -object OfflineMutualFollowExpansionSource { - val Identifier: CandidateSourceIdentifier = - CandidateSourceIdentifier(Algorithm.MutualFollowExpansion.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceFsConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceFsConfig.scala deleted file mode 100644 index 152fc97a6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceFsConfig.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.FSParam -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class OfflineStpSourceFsConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[FSParam[Boolean] with FSName] = Seq( - OfflineStpSourceParams.UseDenserPmiMatrix - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceParams.scala deleted file mode 100644 index fb1672cdb..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceParams.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.timelines.configapi.FSParam - -object OfflineStpSourceParams { - // If enabled, we use the new, denser version of PMI matrix to generate OfflineSTP candidates. - case object UseDenserPmiMatrix - extends FSParam[Boolean]("offline_stp_source_use_denser_pmi_matrix", default = false) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceWithDensePmiMatrix.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceWithDensePmiMatrix.scala deleted file mode 100644 index 6a37ff222..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceWithDensePmiMatrix.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.google.inject.Singleton -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.generated.client.hub.PpmiDenseMatrixCandidatesClientColumn -import javax.inject.Inject - -/** - * Main source for strong-tie-prediction candidates generated offline. - */ -@Singleton -class OfflineStpSourceWithDensePmiMatrix @Inject() ( - stpColumn: PpmiDenseMatrixCandidatesClientColumn) - extends OfflineStrongTiePredictionBaseSource(stpColumn.fetcher) { - override val identifier: CandidateSourceIdentifier = OfflineStpSourceWithDensePmiMatrix.Identifier -} - -object OfflineStpSourceWithDensePmiMatrix { - val Identifier: CandidateSourceIdentifier = - CandidateSourceIdentifier(Algorithm.StrongTiePredictionRec.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceWithLegacyPmiMatrix.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceWithLegacyPmiMatrix.scala deleted file mode 100644 index 7e17b2e8a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStpSourceWithLegacyPmiMatrix.scala +++ /dev/null @@ -1,23 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.google.inject.Singleton -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.generated.client.onboarding.userrecs.StrongTiePredictionClientColumn -import javax.inject.Inject - -/** - * Main source for strong-tie-prediction candidates generated offline. - */ -@Singleton -class OfflineStpSourceWithLegacyPmiMatrix @Inject() ( - stpColumn: StrongTiePredictionClientColumn) - extends OfflineStrongTiePredictionBaseSource(stpColumn.fetcher) { - override val identifier: CandidateSourceIdentifier = - OfflineStpSourceWithLegacyPmiMatrix.Identifier -} - -object OfflineStpSourceWithLegacyPmiMatrix { - val Identifier: CandidateSourceIdentifier = - CandidateSourceIdentifier(Algorithm.StrongTiePredictionRec.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStrongTiePredictionBaseSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStrongTiePredictionBaseSource.scala deleted file mode 100644 index a46d49662..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStrongTiePredictionBaseSource.scala +++ /dev/null @@ -1,57 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FollowProof -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.hermit.stp.thriftscala.STPResult -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.client.Fetcher -import com.twitter.timelines.configapi.HasParams - -/** Base class that all variants of our offline stp dataset can extend. Assumes the same STPResult - * value in the key and converts the result into the necessary internal model. - */ -abstract class OfflineStrongTiePredictionBaseSource( - fetcher: Fetcher[Long, Unit, STPResult]) - extends CandidateSource[HasParams with HasClientContext, CandidateUser] { - - def fetch( - target: Long, - ): Stitch[Seq[CandidateUser]] = { - fetcher - .fetch(target) - .map { result => - result.v - .map { candidates => OfflineStrongTiePredictionBaseSource.map(target, candidates) } - .getOrElse(Nil) - .map(_.withCandidateSource(identifier)) - } - } - - override def apply(request: HasParams with HasClientContext): Stitch[Seq[CandidateUser]] = { - request.getOptionalUserId.map(fetch).getOrElse(Stitch.Nil) - } -} - -object OfflineStrongTiePredictionBaseSource { - def map(target: Long, candidates: STPResult): Seq[CandidateUser] = { - for { - candidate <- candidates.strongTieUsers.sortBy(-_.score) - } yield CandidateUser( - id = candidate.userId, - score = Some(candidate.score), - reason = Some( - Reason( - Some( - AccountProof( - followProof = candidate.socialProof.map(proof => FollowProof(proof, proof.size)) - ) - ) - ) - ) - ) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStrongTiePredictionSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStrongTiePredictionSource.scala deleted file mode 100644 index 6a1cb3983..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OfflineStrongTiePredictionSource.scala +++ /dev/null @@ -1,44 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.google.inject.Singleton -import com.twitter.follow_recommendations.common.candidate_sources.stp.OfflineStpSourceParams.UseDenserPmiMatrix -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.component_library.model.candidate.UserCandidate -import com.twitter.product_mixer.core.feature.Feature -import com.twitter.util.logging.Logging -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject - -object OfflineStpScore extends Feature[UserCandidate, Option[Double]] - -/** - * Main source for strong-tie-prediction candidates generated offline. - */ -@Singleton -class OfflineStrongTiePredictionSource @Inject() ( - offlineStpSourceWithLegacyPmiMatrix: OfflineStpSourceWithLegacyPmiMatrix, - offlineStpSourceWithDensePmiMatrix: OfflineStpSourceWithDensePmiMatrix) - extends CandidateSource[HasParams with HasClientContext, CandidateUser] - with Logging { - override val identifier: CandidateSourceIdentifier = OfflineStrongTiePredictionSource.Identifier - - override def apply(request: HasParams with HasClientContext): Stitch[Seq[CandidateUser]] = { - if (request.params(UseDenserPmiMatrix)) { - logger.info("Using dense PMI matrix.") - offlineStpSourceWithDensePmiMatrix(request) - } else { - logger.info("Using legacy PMI matrix.") - offlineStpSourceWithLegacyPmiMatrix(request) - } - } -} - -object OfflineStrongTiePredictionSource { - val Identifier: CandidateSourceIdentifier = - CandidateSourceIdentifier(Algorithm.StrongTiePredictionRec.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceFSConfig.scala deleted file mode 100644 index ff61cc29c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceFSConfig.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.FSParam -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class OnlineSTPSourceFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[FSParam[Boolean] with FSName] = Seq( - OnlineSTPSourceParams.DisableHeavyRanker, - OnlineSTPSourceParams.UseDBv2Scorer, - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceParams.scala deleted file mode 100644 index e6224d359..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceParams.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.Param - -object OnlineSTPSourceParams { - // This replaces the old scorer module, located at EpStpScorer.scala, with the new scorer, located - // at Dbv2StpScorer.scala. - case object UseDBv2Scorer - extends FSParam[Boolean]("online_stp_source_dbv2_scorer_enabled", default = false) - - // For experiments that test the impact of an improved OnlineSTP source, this controls the usage - // of the PostNux heavy-ranker. Note that this FS should *NOT* trigger bucket impressions. - case object DisableHeavyRanker - extends FSParam[Boolean]("online_stp_source_disable_heavy_ranker", default = false) - - case object SetPredictionDetails extends Param[Boolean](default = false) - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceScorer.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceScorer.scala deleted file mode 100644 index 16bc60776..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceScorer.scala +++ /dev/null @@ -1,29 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class OnlineSTPSourceScorer @Inject() ( - onlineSTPSourceWithEPScorer: OnlineSTPSourceWithEPScorer) - extends CandidateSource[ - HasClientContext with HasParams with HasRecentFollowedUserIds, - CandidateUser - ] { - - override def apply( - request: HasClientContext with HasParams with HasRecentFollowedUserIds - ): Stitch[Seq[CandidateUser]] = { - onlineSTPSourceWithEPScorer(request) - } - - override val identifier: CandidateSourceIdentifier = BaseOnlineSTPSource.Identifier -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceWithDeepbirdV2Scorer.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceWithDeepbirdV2Scorer.scala deleted file mode 100644 index b8f348fea..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceWithDeepbirdV2Scorer.scala +++ /dev/null @@ -1,76 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FollowProof -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.onboarding.relevance.features.strongtie.{ - StrongTieFeatures => StrongTieFeaturesWrapper -} -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.STPRecord -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class OnlineSTPSourceWithDeepbirdV2Scorer @Inject() ( - dbv2StpScorer: Dbv2StpScorer, - stpGraphBuilder: STPGraphBuilder, - baseStatReceiver: StatsReceiver) - extends BaseOnlineSTPSource(stpGraphBuilder, baseStatReceiver) { - - private val dbv2ScorerUsedCounter = statsReceiver.counter("dbv2_scorer_used") - private val dbv2ScorerFailureCounter = statsReceiver.counter("dbv2_scorer_failure") - private val dbv2ScorerSuccessCounter = statsReceiver.counter("dbv2_scorer_success") - - override def getCandidates( - records: Seq[STPRecord], - request: HasClientContext with HasParams with HasRecentFollowedUserIds, - ): Stitch[Seq[CandidateUser]] = { - val possibleCandidates: Seq[Stitch[Option[CandidateUser]]] = records.map { trainingRecord => - dbv2ScorerUsedCounter.incr() - val score = dbv2StpScorer.getScoredResponse(trainingRecord) - score.map { - case None => - dbv2ScorerFailureCounter.incr() - None - case Some(scoreVal) => - dbv2ScorerSuccessCounter.incr() - Some( - CandidateUser( - id = trainingRecord.destinationId, - score = Some(OnlineSTPSourceWithDeepbirdV2Scorer.logitSubtraction(scoreVal)), - reason = Some( - Reason(Some( - AccountProof(followProof = - Some(FollowProof(trainingRecord.socialProof, trainingRecord.socialProof.size))) - ))) - ).withCandidateSourceAndFeatures( - identifier, - Seq(StrongTieFeaturesWrapper(trainingRecord.features))) - ) - } - } - Stitch.collect(possibleCandidates).map { _.flatten.sortBy(-_.score.getOrElse(0.0)) } - } -} - -object OnlineSTPSourceWithDeepbirdV2Scorer { - // The following two variables are the means for the distribution of scores coming from the legacy - // and DBv2 OnlineSTP models. We need this to calibrate the DBv2 scores and align the two means. - // BQ Link: https://console.cloud.google.com/bigquery?sq=213005704923:e06ac27e4db74385a77a4b538c531f82 - private val legacyMeanScore = 0.0478208871192468 - private val dbv2MeanScore = 0.238666097210261 - - // In below are the necessary functions to calibrate the scores such that the means are aligned. - private val EPS: Double = 1e-8 - private val e: Double = math.exp(1) - private def sigmoid(x: Double): Double = math.pow(e, x) / (math.pow(e, x) + 1) - // We add an EPS to the denominator to avoid division by 0. - private def logit(x: Double): Double = math.log(x / (1 - x + EPS)) - def logitSubtraction(x: Double): Double = sigmoid( - logit(x) - (logit(dbv2MeanScore) - logit(legacyMeanScore))) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceWithEPScorer.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceWithEPScorer.scala deleted file mode 100644 index 1c6163852..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/OnlineSTPSourceWithEPScorer.scala +++ /dev/null @@ -1,58 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.candidate_sources.stp.OnlineSTPSourceParams.SetPredictionDetails -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FollowProof -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.onboarding.relevance.features.strongtie.{ - StrongTieFeatures => StrongTieFeaturesWrapper -} -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.logging.Logging -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.STPRecord -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class OnlineSTPSourceWithEPScorer @Inject() ( - epStpScorer: EpStpScorer, - stpGraphBuilder: STPGraphBuilder, - baseStatReceiver: StatsReceiver) - extends BaseOnlineSTPSource(stpGraphBuilder, baseStatReceiver) - with Logging { - private val epScorerUsedCounter = statsReceiver.counter("ep_scorer_used") - - override def getCandidates( - records: Seq[STPRecord], - request: HasClientContext with HasParams with HasRecentFollowedUserIds, - ): Stitch[Seq[CandidateUser]] = { - epScorerUsedCounter.incr() - - val possibleCandidates: Seq[Stitch[Option[CandidateUser]]] = records.map { trainingRecord => - val scoredResponse = - epStpScorer.getScoredResponse(trainingRecord.record, request.params(SetPredictionDetails)) - scoredResponse.map(_.map { response: ScoredResponse => - logger.debug(response) - CandidateUser( - id = trainingRecord.destinationId, - score = Some(response.score), - reason = Some( - Reason( - Some( - AccountProof(followProof = - Some(FollowProof(trainingRecord.socialProof, trainingRecord.socialProof.size))) - ))) - ).withCandidateSourceAndFeatures( - identifier, - Seq(StrongTieFeaturesWrapper(trainingRecord.features))) - }) - } - - Stitch.collect(possibleCandidates).map { _.flatten.sortBy(-_.score.getOrElse(0.0)) } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/README.md deleted file mode 100644 index f3d415d3a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/README.md +++ /dev/null @@ -1,47 +0,0 @@ -# Strong Tie Prediction (STP) Candidate Source -Provides accounts with a high probability of potential mutual follows between the target user and the candidates. - -## STP: Strong Tie Prediction -STP refers to the prediction of p(MutualFollow) for a given pair of users, which powers the concept of People You May Know (PYMK). - -For training, positives are existing mutual follows and negatives are mutual follows of your follows. Features help distinguish between friends and friends-of-friends. - -For inference, candidates are the topK mutuals of your follows. These are rescored, and we only send the topN to the product or next re-ranker. - - -### Online STP -Online STP generates a pool of candidates who are then ranked via a lightweight ranker. -It does this through a two-hop expansion of the mutual follow graph of users, where the first-degree neighbor is another user who has a link with the target user from following types: -* Mutual Follow -* Outbound phone contacts -* Outbound email contacts -* Inbound phone contacts -* Inbound email contacts - -The second-degree neighbor can only be a mutual follow link. - -Currently, online STP can only perform the two-hop expansions on new users (<= 30 days since signup) due to compute resource constraints. - -Features used for the lightweight ranker: -* realGraphWeight: real graph weight between user and first degree nodes -* isForwardEmail: whether the candidate is in the user's email book -* isReverseEmail: whether the user is in the candidate's email book -* isForwardPhonebook: whether the candidate is in the user's phone book -* isReversePhonebook: whether the user is in the candidate's phone book -* numMutualFollowPath: number of mutual follow path between the user and the candidate -* numLowTweepcredFollowPath: number of mutual low TweepCred path between the user and the candidate - * Tweepcred is a social network analysis tool that calculates the influence of Twitter users based on their interactions with other users. The tool uses the PageRank algorithm to rank users based on their influence. -* hasForwardEmailPath: is there a third user x in the user's email book that connect user -> x -> candidate? -* hasReverseEmailPath: is there a third user x in the user's reverse email book that connect user -> x -> candidate? -* hasForwardPhonebookPath: is there a third user x in the user's phonebook that connect user -> x -> candidate? -* hasReversePhonebookPath: is there a third user x in the user's reverse phonebook that connect user -> x -> candidate? - -### Offline STP -Offline STP is powered by Pointwise Mutual Information, which measures the association between two users based on Twitter's mutual follow graph. -An offline job generates candidates based on the overlap between their Mutual and Addressbook Follows and that of the target user. Candidates are then made available online. -Candidates in OfflineSTP are "accounts that have a high overlap of mutually-followed accounts with an account in your follow graph." -This can potentially mean that OfflineSTP has a bigger reach than OnlineSTP. -For example, in the provided diagram, B and C have a high overlap of mutual follows, so it would be considered a candidate for A that is three hops away. -![img.png](img.png) - -Overall, STP is a useful candidate source for generating potential follow recommendations based on strong ties between users, but it should be used in conjunction with other candidate sources and re-rankers to provide a well-rounded set of recommendations for the target user. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPFirstDegreeFetcher.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPFirstDegreeFetcher.scala deleted file mode 100644 index 477e19eb5..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPFirstDegreeFetcher.scala +++ /dev/null @@ -1,155 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.conversions.DurationOps._ -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ForwardEmailBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ForwardPhoneBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ReverseEmailBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ReversePhoneBookSource -import com.twitter.follow_recommendations.common.clients.real_time_real_graph.RealTimeRealGraphClient -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.follow_recommendations.common.models.PotentialFirstDegreeEdge -import com.twitter.follow_recommendations.common.stores.LowTweepCredFollowStore -import com.twitter.hermit.model.Algorithm -import com.twitter.hermit.model.Algorithm.Algorithm -import com.twitter.inject.Logging -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.Duration -import com.twitter.util.Timer -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.FirstDegreeEdge -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.FirstDegreeEdgeInfo -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.FirstDegreeEdgeInfoMonoid -import javax.inject.Inject -import javax.inject.Singleton - -// Grabs FirstDegreeNodes from Candidate Sources -@Singleton -class STPFirstDegreeFetcher @Inject() ( - realTimeGraphClient: RealTimeRealGraphClient, - reversePhoneBookSource: ReversePhoneBookSource, - reverseEmailBookSource: ReverseEmailBookSource, - forwardEmailBookSource: ForwardEmailBookSource, - forwardPhoneBookSource: ForwardPhoneBookSource, - mutualFollowStrongTiePredictionSource: MutualFollowStrongTiePredictionSource, - lowTweepCredFollowStore: LowTweepCredFollowStore, - timer: Timer, - statsReceiver: StatsReceiver) - extends Logging { - - private val stats = statsReceiver.scope("STPFirstDegreeFetcher") - private val stitchRequests = stats.scope("stitchRequests") - private val allStitchRequests = stitchRequests.counter("all") - private val timeoutStitchRequests = stitchRequests.counter("timeout") - private val successStitchRequests = stitchRequests.counter("success") - - private implicit val firstDegreeEdgeInfoMonoid: FirstDegreeEdgeInfoMonoid = - new FirstDegreeEdgeInfoMonoid - - /** - * Used to map from algorithm to the correct fetcher and firstDegreeEdgeInfo. - * Afterward, uses fetcher to get candidates and construct the correct FirstDegreeEdgeInfo. - * */ - private def getPotentialFirstEdgesFromFetcher( - userId: Long, - target: HasClientContext with HasParams with HasRecentFollowedUserIds, - algorithm: Algorithm, - weight: Double - ): Stitch[Seq[PotentialFirstDegreeEdge]] = { - val (candidates, edgeInfo) = algorithm match { - case Algorithm.MutualFollowSTP => - ( - mutualFollowStrongTiePredictionSource(target), - Some(FirstDegreeEdgeInfo(mutualFollow = true))) - case Algorithm.ReverseEmailBookIbis => - (reverseEmailBookSource(target), Some(FirstDegreeEdgeInfo(reverseEmail = true))) - case Algorithm.ReversePhoneBook => - (reversePhoneBookSource(target), Some(FirstDegreeEdgeInfo(reversePhone = true))) - case Algorithm.ForwardEmailBook => - (forwardEmailBookSource(target), Some(FirstDegreeEdgeInfo(forwardEmail = true))) - case Algorithm.ForwardPhoneBook => - (forwardPhoneBookSource(target), Some(FirstDegreeEdgeInfo(forwardPhone = true))) - case Algorithm.LowTweepcredFollow => - ( - lowTweepCredFollowStore.getLowTweepCredUsers(target), - Some(FirstDegreeEdgeInfo(lowTweepcredFollow = true))) - case _ => - (Stitch.Nil, None) - } - candidates.map(_.flatMap { candidate => - edgeInfo.map(PotentialFirstDegreeEdge(userId, candidate.id, algorithm, weight, _)) - }) - } - - /** - * Using the DefaultMap (AlgorithmToScore) we iterate through algorithm/weights to get - * candidates with a set weight. Then, given repeating candidates (by candidate id). - * Given those candidates we group by the candidateId and sum all below weights and combine - * the edgeInfos of into one. Then we choose the candidates with most weight. Finally, - * we attach the realGraphWeight score to those candidates. - * */ - def getFirstDegreeEdges( - target: HasClientContext with HasParams with HasRecentFollowedUserIds - ): Stitch[Seq[FirstDegreeEdge]] = { - target.getOptionalUserId - .map { userId => - allStitchRequests.incr() - val firstEdgesQueryStitch = Stitch - .collect(STPFirstDegreeFetcher.DefaultGraphBuilderAlgorithmToScore.map { - case (algorithm, candidateWeight) => - getPotentialFirstEdgesFromFetcher(userId, target, algorithm, candidateWeight) - }.toSeq) - .map(_.flatten) - - val destinationIdsToEdges = firstEdgesQueryStitch - .map(_.groupBy(_.connectingId).map { - case (destinationId: Long, edges: Seq[PotentialFirstDegreeEdge]) => - val combinedDestScore = edges.map(_.score).sum - val combinedEdgeInfo: FirstDegreeEdgeInfo = - edges.map(_.edgeInfo).fold(firstDegreeEdgeInfoMonoid.zero) { - (aggregatedInfo, currentInfo) => - firstDegreeEdgeInfoMonoid.plus(aggregatedInfo, currentInfo) - } - (destinationId, combinedEdgeInfo, combinedDestScore) - }).map(_.toSeq) - - val topDestinationEdges = destinationIdsToEdges.map(_.sortBy { - case (_, _, combinedDestScore) => -combinedDestScore - }.take(STPFirstDegreeFetcher.MaxNumFirstDegreeEdges)) - - Stitch - .join(realTimeGraphClient.getRealGraphWeights(userId), topDestinationEdges).map { - case (realGraphWeights, topDestinationEdges) => - successStitchRequests.incr() - topDestinationEdges.map { - case (destinationId, combinedEdgeInfo, _) => - val updatedEdgeInfo = combinedEdgeInfo.copy( - realGraphWeight = realGraphWeights.getOrElse(destinationId, 0.0), - lowTweepcredFollow = - !combinedEdgeInfo.mutualFollow && combinedEdgeInfo.lowTweepcredFollow - ) - FirstDegreeEdge(userId, destinationId, updatedEdgeInfo) - } - }.within(STPFirstDegreeFetcher.LongTimeoutFetcher)(timer).rescue { - case ex => - timeoutStitchRequests.incr() - logger.error("Exception while loading direct edges in OnlineSTP: ", ex) - Stitch.Nil - } - }.getOrElse(Stitch.Nil) - } -} - -object STPFirstDegreeFetcher { - val MaxNumFirstDegreeEdges = 200 - val DefaultGraphBuilderAlgorithmToScore = Map( - Algorithm.MutualFollowSTP -> 10.0, - Algorithm.LowTweepcredFollow -> 6.0, - Algorithm.ForwardEmailBook -> 7.0, - Algorithm.ForwardPhoneBook -> 9.0, - Algorithm.ReverseEmailBookIbis -> 5.0, - Algorithm.ReversePhoneBook -> 8.0 - ) - val LongTimeoutFetcher: Duration = 300.millis -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPGraphBuilder.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPGraphBuilder.scala deleted file mode 100644 index 0d2fe7ffc..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPGraphBuilder.scala +++ /dev/null @@ -1,32 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.finagle.stats.Stat -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.follow_recommendations.common.models.STPGraph -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class STPGraphBuilder @Inject() ( - stpFirstDegreeFetcher: STPFirstDegreeFetcher, - stpSecondDegreeFetcher: STPSecondDegreeFetcher, - statsReceiver: StatsReceiver) { - private val stats: StatsReceiver = statsReceiver.scope(this.getClass.getSimpleName) - private val firstDegreeStat: Stat = stats.stat("first_degree_edges") - private val secondDegreeStat: Stat = stats.stat("second_degree_edges") - def apply( - target: HasClientContext with HasParams with HasRecentFollowedUserIds - ): Stitch[STPGraph] = stpFirstDegreeFetcher - .getFirstDegreeEdges(target).flatMap { firstDegreeEdges => - firstDegreeStat.add(firstDegreeEdges.size) - stpSecondDegreeFetcher - .getSecondDegreeEdges(target, firstDegreeEdges).map { secondDegreeEdges => - secondDegreeStat.add(firstDegreeEdges.size) - STPGraph(firstDegreeEdges.toList, secondDegreeEdges.toList) - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPSecondDegreeFetcher.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPSecondDegreeFetcher.scala deleted file mode 100644 index b7e996ab3..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/STPSecondDegreeFetcher.scala +++ /dev/null @@ -1,94 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.twitter.follow_recommendations.common.models.IntermediateSecondDegreeEdge -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.userrecs.StrongTiePredictionFeaturesOnUserClientColumn -import com.twitter.timelines.configapi.HasParams -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.FirstDegreeEdge -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.SecondDegreeEdge -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.SecondDegreeEdgeInfo -import javax.inject.Inject -import javax.inject.Singleton - -// Link to code functionality we're migrating -@Singleton -class STPSecondDegreeFetcher @Inject() ( - strongTiePredictionFeaturesOnUserClientColumn: StrongTiePredictionFeaturesOnUserClientColumn) { - - private def scoreSecondDegreeEdge(edge: SecondDegreeEdge): (Int, Int, Int) = { - def bool2int(b: Boolean): Int = if (b) 1 else 0 - ( - -edge.edgeInfo.numMutualFollowPath, - -edge.edgeInfo.numLowTweepcredFollowPath, - -(bool2int(edge.edgeInfo.forwardEmailPath) + bool2int(edge.edgeInfo.reverseEmailPath) + - bool2int(edge.edgeInfo.forwardPhonePath) + bool2int(edge.edgeInfo.reversePhonePath)) - ) - } - - // Use each first-degree edge(w/ candidateId) to expand and find mutual follows. - // Then, with the mutual follows, group-by candidateId and join edge information - // to create secondDegree edges. - def getSecondDegreeEdges( - target: HasClientContext with HasParams, - firstDegreeEdges: Seq[FirstDegreeEdge] - ): Stitch[Seq[SecondDegreeEdge]] = { - target.getOptionalUserId - .map { userId => - val firstDegreeConnectingIds = firstDegreeEdges.map(_.dstId) - val firstDegreeEdgeInfoMap = firstDegreeEdges.map(e => (e.dstId, e.edgeInfo)).toMap - - val intermediateSecondDegreeEdgesStitch = Stitch - .traverse(firstDegreeConnectingIds) { connectingId => - val stpFeaturesOptStitch = strongTiePredictionFeaturesOnUserClientColumn.fetcher - .fetch(connectingId) - .map(_.v) - stpFeaturesOptStitch.map { stpFeatureOpt => - val intermediateSecondDegreeEdges = for { - edgeInfo <- firstDegreeEdgeInfoMap.get(connectingId) - stpFeatures <- stpFeatureOpt - topSecondDegreeUserIds = - stpFeatures.topMutualFollows - .getOrElse(Nil) - .map(_.userId) - .take(STPSecondDegreeFetcher.MaxNumOfMutualFollows) - } yield topSecondDegreeUserIds.map( - IntermediateSecondDegreeEdge(connectingId, _, edgeInfo)) - intermediateSecondDegreeEdges.getOrElse(Nil) - } - }.map(_.flatten) - - intermediateSecondDegreeEdgesStitch.map { intermediateSecondDegreeEdges => - val secondaryDegreeEdges = intermediateSecondDegreeEdges.groupBy(_.candidateId).map { - case (candidateId, intermediateEdges) => - SecondDegreeEdge( - srcId = userId, - dstId = candidateId, - edgeInfo = SecondDegreeEdgeInfo( - numMutualFollowPath = intermediateEdges.count(_.edgeInfo.mutualFollow), - numLowTweepcredFollowPath = - intermediateEdges.count(_.edgeInfo.lowTweepcredFollow), - forwardEmailPath = intermediateEdges.exists(_.edgeInfo.forwardEmail), - reverseEmailPath = intermediateEdges.exists(_.edgeInfo.reverseEmail), - forwardPhonePath = intermediateEdges.exists(_.edgeInfo.forwardPhone), - reversePhonePath = intermediateEdges.exists(_.edgeInfo.reversePhone), - socialProof = intermediateEdges - .filter { e => e.edgeInfo.mutualFollow || e.edgeInfo.lowTweepcredFollow } - .sortBy(-_.edgeInfo.realGraphWeight) - .take(3) - .map { c => (c.connectingId, c.edgeInfo.realGraphWeight) } - ) - ) - } - secondaryDegreeEdges.toSeq - .sortBy(scoreSecondDegreeEdge) - .take(STPSecondDegreeFetcher.MaxNumSecondDegreeEdges) - } - }.getOrElse(Stitch.Nil) - } -} - -object STPSecondDegreeFetcher { - val MaxNumSecondDegreeEdges = 200 - val MaxNumOfMutualFollows = 50 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/SocialProofEnforcedOfflineStrongTiePredictionSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/SocialProofEnforcedOfflineStrongTiePredictionSource.scala deleted file mode 100644 index 140b8b156..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/SocialProofEnforcedOfflineStrongTiePredictionSource.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.stp - -import com.google.inject.Singleton -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.candidate_sources.base.SocialProofEnforcedCandidateSource -import com.twitter.follow_recommendations.common.transforms.modify_social_proof.ModifySocialProof -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import javax.inject.Inject - -@Singleton -class SocialProofEnforcedOfflineStrongTiePredictionSource @Inject() ( - offlineStrongTiePredictionSource: OfflineStrongTiePredictionSource, - modifySocialProof: ModifySocialProof, - statsReceiver: StatsReceiver) - extends SocialProofEnforcedCandidateSource( - offlineStrongTiePredictionSource, - modifySocialProof, - SocialProofEnforcedOfflineStrongTiePredictionSource.MinNumSocialProofsRequired, - SocialProofEnforcedOfflineStrongTiePredictionSource.Identifier, - statsReceiver) - -object SocialProofEnforcedOfflineStrongTiePredictionSource { - val Identifier = CandidateSourceIdentifier( - Algorithm.StrongTiePredictionRecWithSocialProof.toString) - - val MinNumSocialProofsRequired = 1 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/img.png b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp/img.png deleted file mode 100644 index 1a27bb70e78e49e5e9c32420a50c6fce560662c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 54417 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 z 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 { - organicFollowsAccountsClientColumn.fetcher - .fetch(k) - .map { result => result.v } - } - ) - - /** returns a Seq of ''potential'' content */ - override def apply( - target: TopOrganicFollowsAccountsSource.Target - ): Stitch[Seq[CandidateUser]] = { - if (!target.params(CandidateSourceEnabled)) { - return Stitch.value(Seq[CandidateUser]()) - } - requestsStats.incr() - target.getCountryCode - .orElse(target.geohashAndCountryCode.flatMap(_.countryCode)).map { countryCode => - Stitch - .collect(target - .params(AccountsFilteringAndRankingLogics).map(logic => - cache.readThrough(countryCode.toUpperCase() + "-" + logic))) - .onSuccess(_ => { - successStats.incr() - }) - .onFailure(t => { - debug("candidate source failed identifier = %s".format(identifier), t) - errorStats.incr() - }) - .map(transformOrganicFollowAccountssToCandidateSource) - }.getOrElse { - noCountryCodeStats.incr() - Stitch.value(Seq[CandidateUser]()) - } - } - - private def transformOrganicFollowAccountssToCandidateSource( - organicFollowsAccounts: Seq[Option[OrganicFollowsAccounts]] - ): Seq[CandidateUser] = { - organicFollowsAccounts - .flatMap(opt => - opt - .map(accounts => - accounts.accounts.map(account => - CandidateUser( - id = account.accountId, - score = Some(account.followedCountScore), - ).withCandidateSource(identifier))) - .getOrElse(Seq[CandidateUser]())) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/BUILD deleted file mode 100644 index f69443cf6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/BUILD +++ /dev/null @@ -1,21 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "strato/config/columns/onboarding/userrecs:userrecs-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/README.md deleted file mode 100644 index dcaec2298..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Triangular Loops Candidate Source -Provides account candidates based on the graph structures of the form u -> v -> w -> u, -where the arrow indicates a follow edge. In other words, it looks for triangular loops in the user-user graph. - -If the edge v -> u does not exist in the triangular loop, the Triangular Loops Candidate Source recommends u as a potential outbound mutual follow candidate for v. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsFSConfig.scala deleted file mode 100644 index 444fecd0e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsFSConfig.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.triangular_loops - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.FSParam -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class TriangularLoopsFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[FSParam[Boolean] with FSName] = Nil -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsParams.scala deleted file mode 100644 index 9fb80235e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsParams.scala +++ /dev/null @@ -1,11 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.triangular_loops - -import com.twitter.timelines.configapi.FSParam - -object TriangularLoopsParams { - - object KeepOnlyCandidatesWhoFollowTargetUser - extends FSParam[Boolean]( - "triangular_loops_keep_only_candidates_who_follow_target_user", - false) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsSource.scala deleted file mode 100644 index 73e187ba4..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops/TriangularLoopsSource.scala +++ /dev/null @@ -1,91 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.triangular_loops - -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FollowProof -import com.twitter.follow_recommendations.common.models.HasRecentFollowedByUserIds -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.userrecs.TriangularLoopsV2OnUserClientColumn -import com.twitter.timelines.configapi.HasParams -import com.twitter.wtf.triangular_loop.thriftscala.Candidates -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class TriangularLoopsSource @Inject() ( - triangularLoopsV2Column: TriangularLoopsV2OnUserClientColumn) - extends CandidateSource[ - HasParams with HasClientContext with HasRecentFollowedByUserIds, - CandidateUser - ] { - - override val identifier: CandidateSourceIdentifier = TriangularLoopsSource.Identifier - - override def apply( - target: HasParams with HasClientContext with HasRecentFollowedByUserIds - ): Stitch[Seq[CandidateUser]] = { - val candidates = target.getOptionalUserId - .map { userId => - val fetcher = triangularLoopsV2Column.fetcher - fetcher - .fetch(userId) - .map { result => - result.v - .map(TriangularLoopsSource.mapCandidatesToCandidateUsers) - .getOrElse(Nil) - } - }.getOrElse(Stitch.Nil) - // Make sure recentFollowedByUserIds is populated within the RequestBuilder before enable it - if (target.params(TriangularLoopsParams.KeepOnlyCandidatesWhoFollowTargetUser)) - filterOutCandidatesNotFollowingTargetUser(candidates, target.recentFollowedByUserIds) - else - candidates - } - - def filterOutCandidatesNotFollowingTargetUser( - candidatesStitch: Stitch[Seq[CandidateUser]], - recentFollowings: Option[Seq[Long]] - ): Stitch[Seq[CandidateUser]] = { - candidatesStitch.map { candidates => - val recentFollowingIdsSet = recentFollowings.getOrElse(Nil).toSet - candidates.filter(candidate => recentFollowingIdsSet.contains(candidate.id)) - } - } -} - -object TriangularLoopsSource { - - val Identifier = CandidateSourceIdentifier(Algorithm.TriangularLoop.toString) - val NumResults = 100 - - def mapCandidatesToCandidateUsers(candidates: Candidates): Seq[CandidateUser] = { - candidates.candidates - .map { candidate => - CandidateUser( - id = candidate.incomingUserId, - score = Some(1.0 / math - .max(1, candidate.numFollowers.getOrElse(0) + candidate.numFollowings.getOrElse(0))), - reason = Some( - Reason( - Some( - AccountProof( - followProof = - if (candidate.socialProofUserIds.isEmpty) None - else - Some( - FollowProof( - candidate.socialProofUserIds, - candidate.numSocialProof.getOrElse(candidate.socialProofUserIds.size))) - ) - ) - ) - ) - ).withCandidateSource(Identifier) - }.sortBy(-_.score.getOrElse(0.0)).take(NumResults) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/BUILD deleted file mode 100644 index ad7d6805e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "src/thrift/com/twitter/hermit/candidate:hermit-candidate-scala", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/README.md deleted file mode 100644 index 39c984926..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Two-hop Random Walk -The TwoHopRandomWalk algorithm re-ranks a user's second degree connections based on recent engagement strength. The algorithm works as follows: - -* Given a user `src`, find their top K first degree connections `fd(1)`, `fd(2)`, `fd(3)`,...,`fd(K)`. The ranking is based on real graph weights, which measure the recent engagement strength on the edges. -* For each of the first degree connections `fd(i)`, expand to their top L connections via real graph, `sd(i,1)`, `sd(i,2)`,...,`sd(i,L)`. Note that sd nodes can also be `src`'s first degree nodes. -* Aggregate all the nodes in step 2, filter out the first degree nodes, and calculate the weighted sum for the second degree. -* Re-rank the second degree nodes and select the top M results as the algorithm output. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/TwoHopRandomWalkSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/TwoHopRandomWalkSource.scala deleted file mode 100644 index 5ce2fee70..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk/TwoHopRandomWalkSource.scala +++ /dev/null @@ -1,40 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.two_hop_random_walk - -import com.twitter.follow_recommendations.common.candidate_sources.base.StratoFetcherWithUnitViewSource -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.strato.client.Fetcher -import com.twitter.wtf.candidate.thriftscala.{CandidateSeq => TCandidateSeq} -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -@Singleton -class TwoHopRandomWalkSource @Inject() ( - @Named(GuiceNamedConstants.TWO_HOP_RANDOM_WALK_FETCHER) fetcher: Fetcher[ - Long, - Unit, - TCandidateSeq - ]) extends StratoFetcherWithUnitViewSource[Long, TCandidateSeq]( - fetcher, - TwoHopRandomWalkSource.Identifier) { - - override def map(targetUserId: Long, tCandidateSeq: TCandidateSeq): Seq[CandidateUser] = - TwoHopRandomWalkSource.map(targetUserId, tCandidateSeq) - -} - -object TwoHopRandomWalkSource { - def map(targetUserId: Long, tCandidateSeq: TCandidateSeq): Seq[CandidateUser] = { - tCandidateSeq.candidates - .sortBy(-_.score) - .map { tCandidate => - CandidateUser(id = tCandidate.userId, score = Some(tCandidate.score)) - } - } - - val Identifier: CandidateSourceIdentifier = - CandidateSourceIdentifier(Algorithm.TwoHopRandomWalk.toString) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/BUILD deleted file mode 100644 index 63b10be6c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/BUILD +++ /dev/null @@ -1,18 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "src/thrift/com/twitter/recos/user_user_graph:user_user_graph-scala", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/README.md b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/README.md deleted file mode 100644 index fa510fff7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/README.md +++ /dev/null @@ -1,4 +0,0 @@ -# User-User Graph Candidate Source -Provides account candidates generated from the User-User Graph (UUG). -## User-User Graph (UUG) -The UUG algorithm reads User-Follow-User engagements that occurred in the past 24-48 hours, and provides accounts that the given user's recent followings have recently followed themselves. The UUG algorithm is implemented using the real-time graph processing library GraphJet. diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphCandidateSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphCandidateSource.scala deleted file mode 100644 index 4571f1471..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphCandidateSource.scala +++ /dev/null @@ -1,125 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.user_user_graph - -import com.twitter.finagle.stats.Counter -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.follow_recommendations.common.models._ -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.recos.recos_common.thriftscala.UserSocialProofType -import com.twitter.recos.user_user_graph.thriftscala.RecommendUserDisplayLocation -import com.twitter.recos.user_user_graph.thriftscala.RecommendUserRequest -import com.twitter.recos.user_user_graph.thriftscala.RecommendUserResponse -import com.twitter.recos.user_user_graph.thriftscala.RecommendedUser -import com.twitter.stitch.Stitch -import com.twitter.strato.client.Fetcher -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -@Singleton -class UserUserGraphCandidateSource @Inject() ( - @Named(GuiceNamedConstants.USER_USER_GRAPH_FETCHER) - fetcher: Fetcher[RecommendUserRequest, Unit, RecommendUserResponse], - statsReceiver: StatsReceiver) - extends CandidateSource[ - UserUserGraphCandidateSource.Target, - CandidateUser - ] { - - override val identifier: CandidateSourceIdentifier = UserUserGraphCandidateSource.Identifier - val stats: StatsReceiver = statsReceiver.scope("UserUserGraph") - val requestCounter: Counter = stats.counter("requests") - - override def apply( - target: UserUserGraphCandidateSource.Target - ): Stitch[Seq[CandidateUser]] = { - if (target.params(UserUserGraphParams.UserUserGraphCandidateSourceEnabledInWeightMap)) { - requestCounter.incr() - buildRecommendUserRequest(target) - .map { request => - fetcher - .fetch(request) - .map(_.v) - .map { responseOpt => - responseOpt - .map { response => - response.recommendedUsers - .sortBy(-_.score) - .map(convertToCandidateUsers) - .map(_.withCandidateSource(identifier)) - }.getOrElse(Nil) - } - }.getOrElse(Stitch.Nil) - } else { - Stitch.Nil - } - } - - private[this] def buildRecommendUserRequest( - target: UserUserGraphCandidateSource.Target - ): Option[RecommendUserRequest] = { - (target.getOptionalUserId, target.recentFollowedUserIds) match { - case (Some(userId), Some(recentFollowedUserIds)) => - // use recentFollowedUserIds as seeds for initial experiment - val seedsWithWeights: Map[Long, Double] = recentFollowedUserIds.map { - recentFollowedUserId => - recentFollowedUserId -> UserUserGraphCandidateSource.DefaultSeedWeight - }.toMap - val request = RecommendUserRequest( - requesterId = userId, - displayLocation = UserUserGraphCandidateSource.DisplayLocation, - seedsWithWeights = seedsWithWeights, - excludedUserIds = Some(target.excludedUserIds), - maxNumResults = Some(target.params.getInt(UserUserGraphParams.MaxCandidatesToReturn)), - maxNumSocialProofs = Some(UserUserGraphCandidateSource.MaxNumSocialProofs), - minUserPerSocialProof = Some(UserUserGraphCandidateSource.MinUserPerSocialProof), - socialProofTypes = Some(Seq(UserUserGraphCandidateSource.SocialProofType)) - ) - Some(request) - case _ => None - } - } - - private[this] def convertToCandidateUsers( - recommendedUser: RecommendedUser - ): CandidateUser = { - val socialProofUserIds = - recommendedUser.socialProofs.getOrElse(UserUserGraphCandidateSource.SocialProofType, Nil) - val reasonOpt = if (socialProofUserIds.nonEmpty) { - Some( - Reason( - Some(AccountProof(followProof = - Some(FollowProof(socialProofUserIds, socialProofUserIds.size))))) - ) - } else { - None - } - CandidateUser( - id = recommendedUser.userId, - score = Some(recommendedUser.score), - reason = reasonOpt) - } -} - -object UserUserGraphCandidateSource { - type Target = HasParams - with HasClientContext - with HasRecentFollowedUserIds - with HasExcludedUserIds - - val Identifier: CandidateSourceIdentifier = CandidateSourceIdentifier( - Algorithm.UserUserGraph.toString) - //Use HomeTimeline for experiment - val DisplayLocation: RecommendUserDisplayLocation = RecommendUserDisplayLocation.HomeTimeLine - - //Default params used in MagicRecs - val DefaultSeedWeight: Double = 1.0 - val SocialProofType = UserSocialProofType.Follow - val MaxNumSocialProofs = 10 - val MinUserPerSocialProof: Map[UserSocialProofType, Int] = - Map[UserSocialProofType, Int]((SocialProofType, 2)) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphFSConfig.scala deleted file mode 100644 index 3d3015a36..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphFSConfig.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.user_user_graph - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.Param -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class UserUserGraphFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = Seq( - UserUserGraphParams.UserUserGraphCandidateSourceEnabledInWeightMap, - UserUserGraphParams.UserUserGraphCandidateSourceEnabledInTransform - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphParams.scala deleted file mode 100644 index d146c3459..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph/UserUserGraphParams.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.twitter.follow_recommendations.common.candidate_sources.user_user_graph - -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.Param - -object UserUserGraphParams { - - // max number of candidates to return in total, 50 is the default param used in MagicRecs - object MaxCandidatesToReturn extends Param[Int](default = 50) - - // whether or not to include UserUserGraph candidate source in the weighted blending step - case object UserUserGraphCandidateSourceEnabledInWeightMap - extends FSParam[Boolean]("user_user_graph_candidate_source_enabled_in_weight_map", true) - - // whether or not to include UserUserGraph candidate source in the final transform step - case object UserUserGraphCandidateSourceEnabledInTransform - extends FSParam[Boolean]("user_user_graph_candidate_source_enabled_in_transform", true) - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/AddressbookClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/AddressbookClient.scala deleted file mode 100644 index 6fe1cee6f..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/AddressbookClient.scala +++ /dev/null @@ -1,221 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.addressbook - -import com.twitter.addressbook.datatypes.thriftscala.QueryType -import com.twitter.addressbook.thriftscala.AddressBookGetRequest -import com.twitter.addressbook.thriftscala.AddressBookGetResponse -import com.twitter.addressbook.thriftscala.Addressbook2 -import com.twitter.addressbook.thriftscala.ClientInfo -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.wtf.scalding.jobs.addressbook.thriftscala.STPResultFeature -import com.twitter.follow_recommendations.common.clients.addressbook.models.Contact -import com.twitter.follow_recommendations.common.clients.addressbook.models.EdgeType -import com.twitter.follow_recommendations.common.clients.addressbook.models.QueryOption -import com.twitter.follow_recommendations.common.clients.addressbook.models.RecordIdentifier -import com.twitter.wtf.scalding.jobs.address_book.ABUtil.hashContact -import com.twitter.wtf.scalding.jobs.address_book.ABUtil.normalizeEmail -import com.twitter.wtf.scalding.jobs.address_book.ABUtil.normalizePhoneNumber -import com.twitter.hermit.usercontacts.thriftscala.{UserContacts => tUserContacts} -import com.twitter.stitch.Stitch -import com.twitter.strato.client.Fetcher -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class AddressbookClient @Inject() ( - addressbookService: Addressbook2.MethodPerEndpoint, - statsReceiver: StatsReceiver = NullStatsReceiver) { - - private val stats: StatsReceiver = statsReceiver.scope(this.getClass.getSimpleName) - - private[this] def getResponseFromService( - identifiers: Seq[RecordIdentifier], - batchSize: Int, - edgeType: EdgeType, - maxFetches: Int, - queryOption: Option[QueryOption] - ): Stitch[Seq[AddressBookGetResponse]] = { - Stitch - .collect( - identifiers.map { identifier => - Stitch.callFuture( - addressbookService.get(AddressBookGetRequest( - clientInfo = ClientInfo(None), - identifier = identifier.toThrift, - edgeType = edgeType.toThrift, - queryType = QueryType.UserId, - queryOption = queryOption.map(_.toThrift), - maxFetches = maxFetches, - resultBatchSize = batchSize - ))) - } - ) - } - - private[this] def getContactsResponseFromService( - identifiers: Seq[RecordIdentifier], - batchSize: Int, - edgeType: EdgeType, - maxFetches: Int, - queryOption: Option[QueryOption] - ): Stitch[Seq[AddressBookGetResponse]] = { - Stitch - .collect( - identifiers.map { identifier => - Stitch.callFuture( - addressbookService.get(AddressBookGetRequest( - clientInfo = ClientInfo(None), - identifier = identifier.toThrift, - edgeType = edgeType.toThrift, - queryType = QueryType.Contact, - queryOption = queryOption.map(_.toThrift), - maxFetches = maxFetches, - resultBatchSize = batchSize - ))) - } - ) - } - - /** Mode of addressbook resolving logic - * ManhattanThenABV2: fetching manhattan cached result and backfill with addressbook v2 - * ABV2Only: calling addressbook v2 directly without fetching manhattan cached result - * This can be controlled by passing a fetcher or not. Passing a fetcher will attempt to use it, - * if not then it won't. - */ - def getUsers( - userId: Long, - identifiers: Seq[RecordIdentifier], - batchSize: Int, - edgeType: EdgeType, - fetcherOption: Option[Fetcher[Long, Unit, tUserContacts]] = None, - maxFetches: Int = 1, - queryOption: Option[QueryOption] = None, - ): Stitch[Seq[Long]] = { - fetcherOption match { - case Some(fetcher) => - getUsersFromManhattan(userId, fetcher).flatMap { userContacts => - if (userContacts.isEmpty) { - stats.counter("mhEmptyThenFromAbService").incr() - getResponseFromService(identifiers, batchSize, edgeType, maxFetches, queryOption) - .map(_.flatMap(_.users).flatten.distinct) - } else { - stats.counter("fromManhattan").incr() - Stitch.value(userContacts) - } - } - case None => - stats.counter("fromAbService").incr() - getResponseFromService(identifiers, batchSize, edgeType, maxFetches, queryOption) - .map(_.flatMap(_.users).flatten.distinct) - } - } - - def getHashedContacts( - normalizeFn: String => String, - extractField: String, - )( - userId: Long, - identifiers: Seq[RecordIdentifier], - batchSize: Int, - edgeType: EdgeType, - fetcherOption: Option[Fetcher[String, Unit, STPResultFeature]] = None, - maxFetches: Int = 1, - queryOption: Option[QueryOption] = None, - ): Stitch[Seq[String]] = { - - fetcherOption match { - case Some(fetcher) => - getContactsFromManhattan(userId, fetcher).flatMap { userContacts => - if (userContacts.isEmpty) { - getContactsResponseFromService( - identifiers, - batchSize, - edgeType, - maxFetches, - queryOption) - .map { response => - for { - resp <- response - contacts <- resp.contacts - contactsThrift = contacts.map(Contact.fromThrift) - contactsSet = extractField match { - case "emails" => contactsThrift.flatMap(_.emails.toSeq.flatten) - case "phoneNumbers" => contactsThrift.flatMap(_.phoneNumbers.toSeq.flatten) - } - hashedAndNormalizedContacts = contactsSet.map(c => hashContact(normalizeFn(c))) - } yield hashedAndNormalizedContacts - }.map(_.flatten) - } else { - Stitch.Nil - } - } - case None => { - getContactsResponseFromService(identifiers, batchSize, edgeType, maxFetches, queryOption) - .map { response => - for { - resp <- response - contacts <- resp.contacts - contactsThrift = contacts.map(Contact.fromThrift) - contactsSet = extractField match { - case "emails" => contactsThrift.flatMap(_.emails.toSeq.flatten) - case "phoneNumbers" => contactsThrift.flatMap(_.phoneNumbers.toSeq.flatten) - } - hashedAndNormalizedContacts = contactsSet.map(c => hashContact(normalizeFn(c))) - } yield hashedAndNormalizedContacts - }.map(_.flatten) - } - } - } - - def getEmailContacts = getHashedContacts(normalizeEmail, "emails") _ - def getPhoneContacts = getHashedContacts(normalizePhoneNumber, "phoneNumbers") _ - - private def getUsersFromManhattan( - userId: Long, - fetcher: Fetcher[Long, Unit, tUserContacts], - ): Stitch[Seq[Long]] = fetcher - .fetch(userId) - .map(_.v.map(_.destinationIds).toSeq.flatten.distinct) - - private def getContactsFromManhattan( - userId: Long, - fetcher: Fetcher[String, Unit, STPResultFeature], - ): Stitch[Seq[String]] = fetcher - .fetch(userId.toString) - .map(_.v.map(_.strongTieUserFeature.map(_.destId)).toSeq.flatten.distinct) -} - -object AddressbookClient { - val AddressBook2BatchSize = 500 - - def createQueryOption(edgeType: EdgeType, isPhone: Boolean): Option[QueryOption] = - (edgeType, isPhone) match { - case (EdgeType.Reverse, _) => - None - case (EdgeType.Forward, true) => - Some( - QueryOption( - onlyDiscoverableInExpansion = false, - onlyConfirmedInExpansion = false, - onlyDiscoverableInResult = false, - onlyConfirmedInResult = false, - fetchGlobalApiNamespace = false, - isDebugRequest = false, - resolveEmails = false, - resolvePhoneNumbers = true - )) - case (EdgeType.Forward, false) => - Some( - QueryOption( - onlyDiscoverableInExpansion = false, - onlyConfirmedInExpansion = false, - onlyDiscoverableInResult = false, - onlyConfirmedInResult = false, - fetchGlobalApiNamespace = false, - isDebugRequest = false, - resolveEmails = true, - resolvePhoneNumbers = false - )) - } - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/AddressbookModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/AddressbookModule.scala deleted file mode 100644 index 97b841dd2..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/AddressbookModule.scala +++ /dev/null @@ -1,10 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.addressbook - -import com.twitter.addressbook.thriftscala.Addressbook2 -import com.twitter.finatra.mtls.thriftmux.modules.MtlsClient -import com.twitter.follow_recommendations.common.clients.common.BaseClientModule - -object AddressbookModule extends BaseClientModule[Addressbook2.MethodPerEndpoint] with MtlsClient { - override val label = "addressbook" - override val dest = "/s/addressbook/addressbook2" -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/BUILD deleted file mode 100644 index b94cf3efe..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/BUILD +++ /dev/null @@ -1,21 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "addressbook/thrift/src/thrift/com/twitter/addressbook:thrift-scala", - "addressbook/thrift/src/thrift/com/twitter/addressbook/datatypes:thrift-scala", - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/inject/inject-thrift-client", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "src/scala/com/twitter/wtf/scalding/jobs/address_book:ab_util", - "src/thrift/com/twitter/hermit/usercontacts:hermit-usercontacts-scala", - "src/thrift/com/twitter/wtf/addressbook:addressbook-scala", - "stitch/stitch-core", - "strato/src/main/scala/com/twitter/strato/client", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/BUILD deleted file mode 100644 index 885055e2c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/BUILD +++ /dev/null @@ -1,10 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "addressbook/thrift/src/thrift/com/twitter/addressbook:thrift-scala", - "addressbook/thrift/src/thrift/com/twitter/addressbook/datatypes:thrift-scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/Contact.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/Contact.scala deleted file mode 100644 index 3ff39fa32..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/Contact.scala +++ /dev/null @@ -1,29 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.addressbook.models - -import com.twitter.addressbook.{thriftscala => t} -import com.twitter.util.Time - -case class Contact( - id: Long, - emails: Option[Set[String]], - phoneNumbers: Option[Set[String]], - firstName: Option[String], - lastName: Option[String], - name: Option[String], - appId: Option[Long], - appIds: Option[Set[Long]], - importedTimestamp: Option[Time]) - -object Contact { - def fromThrift(thriftContact: t.Contact): Contact = Contact( - thriftContact.id, - thriftContact.emails.map(_.toSet), - thriftContact.phoneNumbers.map(_.toSet), - thriftContact.firstName, - thriftContact.lastName, - thriftContact.name, - thriftContact.appId, - thriftContact.appIds.map(_.toSet), - thriftContact.importedTimestamp.map(Time.fromMilliseconds) - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/EdgeType.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/EdgeType.scala deleted file mode 100644 index 6cfcd65cd..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/EdgeType.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.addressbook.models - -import com.twitter.addressbook.datatypes.{thriftscala => t} - -sealed trait EdgeType { - def toThrift: t.EdgeType -} - -object EdgeType { - case object Forward extends EdgeType { - override val toThrift: t.EdgeType = t.EdgeType.Forward - } - case object Reverse extends EdgeType { - override val toThrift: t.EdgeType = t.EdgeType.Reverse - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/QueryOption.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/QueryOption.scala deleted file mode 100644 index a17c163cb..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/QueryOption.scala +++ /dev/null @@ -1,24 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.addressbook.models - -import com.twitter.addressbook.{thriftscala => t} - -case class QueryOption( - onlyDiscoverableInExpansion: Boolean, - onlyConfirmedInExpansion: Boolean, - onlyDiscoverableInResult: Boolean, - onlyConfirmedInResult: Boolean, - fetchGlobalApiNamespace: Boolean, - isDebugRequest: Boolean, - resolveEmails: Boolean, - resolvePhoneNumbers: Boolean) { - def toThrift: t.QueryOption = t.QueryOption( - onlyDiscoverableInExpansion, - onlyConfirmedInExpansion, - onlyDiscoverableInResult, - onlyConfirmedInResult, - fetchGlobalApiNamespace, - isDebugRequest, - resolveEmails, - resolvePhoneNumbers - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/RecordIdentifier.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/RecordIdentifier.scala deleted file mode 100644 index 0154c71dc..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook/models/RecordIdentifier.scala +++ /dev/null @@ -1,10 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.addressbook.models - -import com.twitter.addressbook.datatypes.{thriftscala => t} - -case class RecordIdentifier( - userId: Option[Long], - email: Option[String], - phoneNumber: Option[String]) { - def toThrift: t.RecordIdentifier = t.RecordIdentifier(userId, email, phoneNumber) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdRequest.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdRequest.scala deleted file mode 100644 index 07cfcdaf5..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdRequest.scala +++ /dev/null @@ -1,45 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.adserver - -import com.twitter.adserver.{thriftscala => t} -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext - -case class AdRequest( - clientContext: ClientContext, - displayLocation: DisplayLocation, - isTest: Option[Boolean], - profileUserId: Option[Long]) { - def toThrift: t.AdRequestParams = { - - val request = t.AdRequest( - displayLocation = displayLocation.toAdDisplayLocation.getOrElse( - throw new MissingAdDisplayLocation(displayLocation)), - isTest = isTest, - countImpressionsOnCallback = Some(true), - numOrganicItems = Some(AdRequest.DefaultNumOrganicItems.toShort), - profileUserId = profileUserId - ) - - val clientInfo = t.ClientInfo( - clientId = clientContext.appId.map(_.toInt), - userIp = clientContext.ipAddress, - userId64 = clientContext.userId, - guestId = clientContext.guestId, - userAgent = clientContext.userAgent, - referrer = None, - deviceId = clientContext.deviceId, - languageCode = clientContext.languageCode, - countryCode = clientContext.countryCode - ) - - t.AdRequestParams(request, clientInfo) - } -} - -object AdRequest { - val DefaultNumOrganicItems = 10 -} - -class MissingAdDisplayLocation(displayLocation: DisplayLocation) - extends Exception( - s"Display Location ${displayLocation.toString} has no mapped AdsDisplayLocation set.") diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdserverClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdserverClient.scala deleted file mode 100644 index 927c0784c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdserverClient.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.adserver - -import com.twitter.adserver.thriftscala.NewAdServer -import com.twitter.adserver.{thriftscala => t} -import com.twitter.stitch.Stitch -import javax.inject.{Inject, Singleton} - -@Singleton -class AdserverClient @Inject() (adserverService: NewAdServer.MethodPerEndpoint) { - def getAdImpressions(adRequest: AdRequest): Stitch[Seq[t.AdImpression]] = { - Stitch - .callFuture( - adserverService.makeAdRequest(adRequest.toThrift) - ).map(_.impressions) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdserverModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdserverModule.scala deleted file mode 100644 index 9a425c058..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/AdserverModule.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.adserver - -import com.twitter.adserver.thriftscala.NewAdServer -import com.twitter.conversions.DurationOps._ -import com.twitter.finagle.ThriftMux -import com.twitter.finatra.mtls.thriftmux.modules.MtlsClient -import com.twitter.follow_recommendations.common.clients.common.BaseClientModule - -object AdserverModule extends BaseClientModule[NewAdServer.MethodPerEndpoint] with MtlsClient { - override val label = "adserver" - override val dest = "/s/ads/adserver" - - override def configureThriftMuxClient(client: ThriftMux.Client): ThriftMux.Client = - client.withRequestTimeout(500.millis) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/BUILD deleted file mode 100644 index c90656afe..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/inject/inject-thrift-client", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "src/thrift/com/twitter/ads/adserver:adserver_rpc-scala", - "stitch/stitch-core", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/BUILD deleted file mode 100644 index a32739651..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/BUILD +++ /dev/null @@ -1,15 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "cache/client", - "finagle/finagle-memcached/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "finatra/inject/inject-thrift-client/src/main/scala", - "stitch/stitch-core", - "util/util-core:scala", - "util/util-thrift", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/MemcacheClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/MemcacheClient.scala deleted file mode 100644 index d00aef475..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/MemcacheClient.scala +++ /dev/null @@ -1,121 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.cache - -import com.twitter.bijection.Bijection -import com.twitter.conversions.DurationOps._ -import com.twitter.finagle.Memcached.Client -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.finagle.util.DefaultTimer -import com.twitter.io.Buf -import com.twitter.stitch.Stitch -import com.twitter.util.Duration -import com.twitter.util.Future -import com.twitter.util.Time -import java.security.MessageDigest - -object MemcacheClient { - def apply[V]( - client: Client, - dest: String, - valueBijection: Bijection[Buf, V], - ttl: Duration, - statsReceiver: StatsReceiver - ): MemcacheClient[V] = { - new MemcacheClient(client, dest, valueBijection, ttl, statsReceiver) - } -} - -class MemcacheClient[V]( - client: Client, - dest: String, - valueBijection: Bijection[Buf, V], - ttl: Duration, - statsReceiver: StatsReceiver) { - val cache = client.newRichClient(dest).adapt[V](valueBijection) - val cacheTtl = Time.fromSeconds(ttl.inSeconds) - - /** - * If cache contains key, return value from cache. Otherwise, run the underlying call - * to fetch the value, store it in cache, and then return the value. - */ - def readThrough( - key: String, - underlyingCall: () => Stitch[V] - ): Stitch[V] = { - val cachedResult: Stitch[Option[V]] = Stitch - .callFuture(getIfPresent(key)) - .within(70.millisecond)(DefaultTimer) - .rescue { - case e: Exception => - statsReceiver.scope("rescued").counter(e.getClass.getSimpleName).incr() - Stitch(None) - } - val resultStitch = cachedResult.map { resultOption => - resultOption match { - case Some(cacheValue) => Stitch.value(cacheValue) - case None => - val underlyingCallStitch = profileStitch( - underlyingCall(), - statsReceiver.scope("underlyingCall") - ) - underlyingCallStitch.map { result => - put(key, result) - result - } - } - }.flatten - // profile the overall Stitch, and return the result - profileStitch(resultStitch, statsReceiver.scope("readThrough")) - } - - def getIfPresent(key: String): Future[Option[V]] = { - cache - .get(hashString(key)) - .onSuccess { - case Some(value) => statsReceiver.counter("cache_hits").incr() - case None => statsReceiver.counter("cache_misses").incr() - } - .onFailure { - case e: Exception => - statsReceiver.counter("cache_misses").incr() - statsReceiver.scope("rescued").counter(e.getClass.getSimpleName).incr() - } - .rescue { - case _ => Future.None - } - } - - def put(key: String, value: V): Future[Unit] = { - cache.set(hashString(key), 0, cacheTtl, value) - } - - /** - * Hash the input key string to a fixed length format using SHA-256 hash function. - */ - def hashString(input: String): String = { - val bytes = MessageDigest.getInstance("SHA-256").digest(input.getBytes("UTF-8")) - bytes.map("%02x".format(_)).mkString - } - - /** - * Helper function for timing a stitch, returning the original stitch. - * - * Defining the profiling function here to keep the dependencies of this class - * generic and easy to export (i.e. copy-and-paste) into other services or packages. - */ - def profileStitch[T](stitch: Stitch[T], stat: StatsReceiver): Stitch[T] = { - Stitch - .time(stitch) - .map { - case (response, stitchRunDuration) => - stat.counter("requests").incr() - stat.stat("latency_ms").add(stitchRunDuration.inMilliseconds) - response - .onSuccess { _ => stat.counter("success").incr() } - .onFailure { e => - stat.counter("failures").incr() - stat.scope("failures").counter(e.getClass.getSimpleName).incr() - } - } - .lowerFromTry - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/MemcacheModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/MemcacheModule.scala deleted file mode 100644 index e85d12f20..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/MemcacheModule.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.cache - -import com.google.inject.Provides -import com.twitter.conversions.DurationOps._ -import com.twitter.finagle.Memcached -import com.twitter.finagle.Memcached.Client -import com.twitter.finagle.mtls.client.MtlsStackClient._ -import com.twitter.finagle.mtls.authentication.ServiceIdentifier -import com.twitter.finagle.service.Retries -import com.twitter.finagle.service.RetryPolicy -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.inject.TwitterModule -import javax.inject.Singleton - -object MemcacheModule extends TwitterModule { - @Provides - @Singleton - def provideMemcacheClient( - serviceIdentifier: ServiceIdentifier, - statsReceiver: StatsReceiver, - ): Client = { - Memcached.client - .withMutualTls(serviceIdentifier) - .withStatsReceiver(statsReceiver.scope("twemcache")) - .withTransport.connectTimeout(1.seconds) - .withRequestTimeout(1.seconds) - .withSession.acquisitionTimeout(10.seconds) - .configured(Retries.Policy(RetryPolicy.tries(1))) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/ThriftBijection.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/ThriftBijection.scala deleted file mode 100644 index 62e36bc33..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache/ThriftBijection.scala +++ /dev/null @@ -1,81 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.cache - -import com.twitter.bijection.Bijection -import com.twitter.io.Buf -import com.twitter.scrooge.CompactThriftSerializer -import com.twitter.scrooge.ThriftEnum -import com.twitter.scrooge.ThriftStruct -import java.nio.ByteBuffer - -abstract class ThriftBijection[T <: ThriftStruct] extends Bijection[Buf, T] { - val serializer: CompactThriftSerializer[T] - - override def apply(b: Buf): T = { - val byteArray = Buf.ByteArray.Owned.extract(b) - serializer.fromBytes(byteArray) - } - - override def invert(a: T): Buf = { - val byteArray = serializer.toBytes(a) - Buf.ByteArray.Owned(byteArray) - } -} - -abstract class ThriftOptionBijection[T <: ThriftStruct] extends Bijection[Buf, Option[T]] { - val serializer: CompactThriftSerializer[T] - - override def apply(b: Buf): Option[T] = { - if (b.isEmpty) { - None - } else { - val byteArray = Buf.ByteArray.Owned.extract(b) - Some(serializer.fromBytes(byteArray)) - } - } - - override def invert(a: Option[T]): Buf = { - a match { - case Some(t) => - val byteArray = serializer.toBytes(t) - Buf.ByteArray.Owned(byteArray) - case None => Buf.Empty - } - } -} - -class ThriftEnumBijection[T <: ThriftEnum](constructor: Int => T) extends Bijection[Buf, T] { - override def apply(b: Buf): T = { - val byteArray = Buf.ByteArray.Owned.extract(b) - val byteBuffer = ByteBuffer.wrap(byteArray) - constructor(byteBuffer.getInt()) - } - - override def invert(a: T): Buf = { - val byteBuffer: ByteBuffer = ByteBuffer.allocate(4) - byteBuffer.putInt(a.getValue) - Buf.ByteArray.Owned(byteBuffer.array()) - } -} - -class ThriftEnumOptionBijection[T <: ThriftEnum](constructor: Int => T) extends Bijection[Buf, Option[T]] { - override def apply(b: Buf): Option[T] = { - if (b.isEmpty) { - None - } else { - val byteArray = Buf.ByteArray.Owned.extract(b) - val byteBuffer = ByteBuffer.wrap(byteArray) - Some(constructor(byteBuffer.getInt())) - } - } - - override def invert(a: Option[T]): Buf = { - a match { - case Some(obj) => { - val byteBuffer: ByteBuffer = ByteBuffer.allocate(4) - byteBuffer.putInt(obj.getValue) - Buf.ByteArray.Owned(byteBuffer.array()) - } - case None => Buf.Empty - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common/BUILD deleted file mode 100644 index 330981d80..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common/BUILD +++ /dev/null @@ -1,11 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/inject/inject-thrift-client", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common/BaseClientModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common/BaseClientModule.scala deleted file mode 100644 index 43949ea19..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common/BaseClientModule.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.common - -import com.twitter.finagle.ThriftMux -import com.twitter.finagle.thrift.Protocols -import com.twitter.follow_recommendations.common.constants.ServiceConstants._ -import com.twitter.inject.thrift.modules.ThriftClientModule -import scala.reflect.ClassTag - -/** - * basic client configurations that we apply for all of our clients go in here - */ -abstract class BaseClientModule[T: ClassTag] extends ThriftClientModule[T] { - def configureThriftMuxClient(client: ThriftMux.Client): ThriftMux.Client = { - client - .withProtocolFactory( - Protocols.binaryFactory( - stringLengthLimit = StringLengthLimit, - containerLengthLimit = ContainerLengthLimit)) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2/BUILD deleted file mode 100644 index daf4c63fa..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "cortex-deepbird/prediction/src/main/scala/com/twitter/cortex/deepbird/prediction", - "cortex-deepbird/thrift/src/main/thrift:thrift-java", - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "finatra/inject/inject-thrift-client/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common", - "src/scala/com/twitter/ml/api/util", - "util/util-core:scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2/DeepBirdV2PredictionServiceClientModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2/DeepBirdV2PredictionServiceClientModule.scala deleted file mode 100644 index 8f707910b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2/DeepBirdV2PredictionServiceClientModule.scala +++ /dev/null @@ -1,67 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.deepbirdv2 - -import com.google.inject.Provides -import com.google.inject.name.Named -import com.twitter.bijection.scrooge.TBinaryProtocol -import com.twitter.conversions.DurationOps._ -import com.twitter.cortex.deepbird.thriftjava.DeepbirdPredictionService -import com.twitter.finagle.ThriftMux -import com.twitter.finagle.builder.ClientBuilder -import com.twitter.finagle.mtls.authentication.ServiceIdentifier -import com.twitter.finagle.mtls.client.MtlsStackClient._ -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.finagle.thrift.ClientId -import com.twitter.finagle.thrift.RichClientParam -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.inject.TwitterModule - -/** - * Module that provides multiple deepbirdv2 prediction service clients - * We use the java api since data records are native java objects and we want to reduce overhead - * while serializing/deserializing data. - */ -object DeepBirdV2PredictionServiceClientModule extends TwitterModule { - - val RequestTimeout = 300.millis - - private def getDeepbirdPredictionServiceClient( - clientId: ClientId, - label: String, - dest: String, - statsReceiver: StatsReceiver, - serviceIdentifier: ServiceIdentifier - ): DeepbirdPredictionService.ServiceToClient = { - val clientStatsReceiver = statsReceiver.scope("clnt") - val mTlsClient = ThriftMux.client.withClientId(clientId).withMutualTls(serviceIdentifier) - new DeepbirdPredictionService.ServiceToClient( - ClientBuilder() - .name(label) - .stack(mTlsClient) - .dest(dest) - .requestTimeout(RequestTimeout) - .reportHostStats(NullStatsReceiver) - .build(), - RichClientParam( - new TBinaryProtocol.Factory(), - clientStats = clientStatsReceiver - ) - ) - } - - @Provides - @Named(GuiceNamedConstants.WTF_PROD_DEEPBIRDV2_CLIENT) - def providesWtfProdDeepbirdV2PredictionService( - clientId: ClientId, - statsReceiver: StatsReceiver, - serviceIdentifier: ServiceIdentifier - ): DeepbirdPredictionService.ServiceToClient = { - getDeepbirdPredictionServiceClient( - clientId = clientId, - label = "WtfProdDeepbirdV2PredictionService", - dest = "/s/cassowary/deepbirdv2-hermit-wtf", - statsReceiver = statsReceiver, - serviceIdentifier = serviceIdentifier - ) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/dismiss_store/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/dismiss_store/BUILD deleted file mode 100644 index 33d8ba841..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/dismiss_store/BUILD +++ /dev/null @@ -1,19 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/github/nscala_time", - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/thrift/src/main/thrift:thrift-scala", - "src/thrift/com/twitter/onboarding/relevance/store:store-scala", - "util/util-core:scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/dismiss_store/DismissStore.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/dismiss_store/DismissStore.scala deleted file mode 100644 index 1c8c9f8fd..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/dismiss_store/DismissStore.scala +++ /dev/null @@ -1,60 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.dismiss_store - -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.onboarding.relevance.store.thriftscala.WhoToFollowDismissEventDetails -import com.twitter.stitch.Stitch -import com.twitter.strato.catalog.Scan.Slice -import com.twitter.strato.client.Scanner -import com.twitter.util.logging.Logging -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -/** - * this store gets the list of dismissed candidates since a certain time - * primarily used for filtering out accounts that a user has explicitly dismissed - * - * we fail open on timeouts, but loudly on other errors - */ -@Singleton -class DismissStore @Inject() ( - @Named(GuiceNamedConstants.DISMISS_STORE_SCANNER) - scanner: Scanner[(Long, Slice[ - (Long, Long) - ]), Unit, (Long, (Long, Long)), WhoToFollowDismissEventDetails], - stats: StatsReceiver) - extends Logging { - - private val MaxCandidatesToReturn = 100 - - // gets a list of dismissed candidates. if numCandidatesToFetchOption is none, we will fetch the default number of candidates - def get( - userId: Long, - negStartTimeMs: Long, - maxCandidatesToFetchOption: Option[Int] - ): Stitch[Seq[Long]] = { - - val maxCandidatesToFetch = maxCandidatesToFetchOption.getOrElse(MaxCandidatesToReturn) - - scanner - .scan( - ( - userId, - Slice( - from = None, - to = Some((negStartTimeMs, Long.MaxValue)), - limit = Some(maxCandidatesToFetch) - ) - ) - ) - .map { - case s: Seq[((Long, (Long, Long)), WhoToFollowDismissEventDetails)] if s.nonEmpty => - s.map { - case ((_: Long, (_: Long, candidateId: Long)), _: WhoToFollowDismissEventDetails) => - candidateId - } - case _ => Nil - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/BUILD deleted file mode 100644 index 78a06d536..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "emailstorage/server/src/main/thrift/com/twitter/emailstorage/api:email-storage-service-scala", - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/inject/inject-thrift-client", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "stitch/stitch-core", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/EmailStorageServiceClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/EmailStorageServiceClient.scala deleted file mode 100644 index 4cf66c658..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/EmailStorageServiceClient.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.email_storage_service - -import com.twitter.cds.contact_consent_state.thriftscala.PurposeOfProcessing -import com.twitter.emailstorage.api.thriftscala.EmailStorageService -import com.twitter.emailstorage.api.thriftscala.GetUsersEmailsRequest -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class EmailStorageServiceClient @Inject() ( - val emailStorageService: EmailStorageService.MethodPerEndpoint) { - - def getVerifiedEmail( - userId: Long, - purposeOfProcessing: PurposeOfProcessing - ): Stitch[Option[String]] = { - val req = GetUsersEmailsRequest( - userIds = Seq(userId), - clientIdentifier = Some("follow-recommendations-service"), - purposesOfProcessing = Some(Seq(purposeOfProcessing)) - ) - - Stitch.callFuture(emailStorageService.getUsersEmails(req)) map { - _.usersEmails.map(_.confirmedEmail.map(_.email)).head - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/EmailStorageServiceModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/EmailStorageServiceModule.scala deleted file mode 100644 index 0a1be9d1b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service/EmailStorageServiceModule.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.email_storage_service - -import com.twitter.emailstorage.api.thriftscala.EmailStorageService -import com.twitter.finatra.mtls.thriftmux.modules.MtlsClient -import com.twitter.follow_recommendations.common.clients.common.BaseClientModule - -object EmailStorageServiceModule - extends BaseClientModule[EmailStorageService.MethodPerEndpoint] - with MtlsClient { - override val label = "email-storage-service" - override val dest = "/s/email-server/email-server" -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/BUILD deleted file mode 100644 index d67fa01d8..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/BUILD +++ /dev/null @@ -1,22 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/github/nscala_time", - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "finatra/inject/inject-thrift-client/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "src/thrift/com/twitter/geoduck:geoduck-scala", - "src/thrift/com/twitter/geoduck:geoduckpartnerplaces-thrift-scala", - "stitch/stitch-core", - "util/util-core:scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/LocationServiceClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/LocationServiceClient.scala deleted file mode 100644 index 69207537c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/LocationServiceClient.scala +++ /dev/null @@ -1,62 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.geoduck - -import com.twitter.follow_recommendations.common.models.GeohashAndCountryCode -import com.twitter.geoduck.common.thriftscala.LocationSource -import com.twitter.geoduck.common.thriftscala.PlaceQuery -import com.twitter.geoduck.common.thriftscala.TransactionLocation -import com.twitter.geoduck.common.thriftscala.UserLocationRequest -import com.twitter.geoduck.thriftscala.LocationService -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class LocationServiceClient @Inject() (locationService: LocationService.MethodPerEndpoint) { - def getGeohashAndCountryCode(userId: Long): Stitch[GeohashAndCountryCode] = { - Stitch - .callFuture { - locationService - .userLocation( - UserLocationRequest( - Seq(userId), - Some(PlaceQuery(allPlaceTypes = Some(true))), - simpleReverseGeocode = true)) - .map(_.found.get(userId)).map { transactionLocationOpt => - val geohashOpt = transactionLocationOpt.flatMap(getGeohashFromTransactionLocation) - val countryCodeOpt = - transactionLocationOpt.flatMap(_.simpleRgcResult.flatMap(_.countryCodeAlpha2)) - GeohashAndCountryCode(geohashOpt, countryCodeOpt) - } - } - } - - private[this] def getGeohashFromTransactionLocation( - transactionLocation: TransactionLocation - ): Option[String] = { - transactionLocation.geohash.flatMap { geohash => - val geohashPrefixLength = transactionLocation.locationSource match { - // if location source is logical, keep the first 4 chars in geohash - case Some(LocationSource.Logical) => Some(4) - // if location source is physical, keep the prefix according to accuracy - // accuracy is the accuracy of GPS readings in the unit of meter - case Some(LocationSource.Physical) => - transactionLocation.coordinate.flatMap { coordinate => - coordinate.accuracy match { - case Some(accuracy) if (accuracy < 50) => Some(7) - case Some(accuracy) if (accuracy < 200) => Some(6) - case Some(accuracy) if (accuracy < 1000) => Some(5) - case Some(accuracy) if (accuracy < 50000) => Some(4) - case Some(accuracy) if (accuracy < 100000) => Some(3) - case _ => None - } - } - case Some(LocationSource.Model) => Some(4) - case _ => None - } - geohashPrefixLength match { - case Some(l: Int) => geohash.stringGeohash.map(_.take(l)) - case _ => None - } - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/LocationServiceModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/LocationServiceModule.scala deleted file mode 100644 index a5fc79a80..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/LocationServiceModule.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.geoduck - -import com.twitter.finatra.mtls.thriftmux.modules.MtlsClient -import com.twitter.follow_recommendations.common.clients.common.BaseClientModule -import com.twitter.geoduck.thriftscala.LocationService - -object LocationServiceModule - extends BaseClientModule[LocationService.MethodPerEndpoint] - with MtlsClient { - override val label = "geoduck_locationservice" - override val dest = "/s/geo/geoduck_locationservice" -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/ReverseGeocodeClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/ReverseGeocodeClient.scala deleted file mode 100644 index 576359128..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/ReverseGeocodeClient.scala +++ /dev/null @@ -1,57 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.geoduck - -import com.twitter.follow_recommendations.common.models.GeohashAndCountryCode -import com.twitter.geoduck.common.thriftscala.Location -import com.twitter.geoduck.common.thriftscala.PlaceQuery -import com.twitter.geoduck.common.thriftscala.ReverseGeocodeIPRequest -import com.twitter.geoduck.service.thriftscala.GeoContext -import com.twitter.geoduck.thriftscala.ReverseGeocoder -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ReverseGeocodeClient @Inject() (rgcService: ReverseGeocoder.MethodPerEndpoint) { - def getGeohashAndCountryCode(ipAddress: String): Stitch[GeohashAndCountryCode] = { - Stitch - .callFuture { - rgcService - .reverseGeocodeIp( - ReverseGeocodeIPRequest( - Seq(ipAddress), - PlaceQuery(None), - simpleReverseGeocode = true - ) // note: simpleReverseGeocode means that country code will be included in response - ).map { response => - response.found.get(ipAddress) match { - case Some(location) => getGeohashAndCountryCodeFromLocation(location) - case _ => GeohashAndCountryCode(None, None) - } - } - } - } - - private def getGeohashAndCountryCodeFromLocation(location: Location): GeohashAndCountryCode = { - val countryCode: Option[String] = location.simpleRgcResult.flatMap { _.countryCodeAlpha2 } - - val geohashString: Option[String] = location.geohash.flatMap { hash => - hash.stringGeohash.flatMap { hashString => - Some(ReverseGeocodeClient.truncate(hashString)) - } - } - - GeohashAndCountryCode(geohashString, countryCode) - } - -} - -object ReverseGeocodeClient { - - val DefaultGeoduckIPRequestContext: GeoContext = - GeoContext(allPlaceTypes = true, includeGeohash = true, includeCountryCode = true) - - // All these geohashes are guessed by IP (Logical Location Source). - // So take the four letters to make sure it is consistent with LocationServiceClient - val GeohashLengthAfterTruncation = 4 - def truncate(geohash: String): String = geohash.take(GeohashLengthAfterTruncation) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/UserLocationFetcher.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/UserLocationFetcher.scala deleted file mode 100644 index 706ae5143..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck/UserLocationFetcher.scala +++ /dev/null @@ -1,59 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.geoduck - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.GeohashAndCountryCode -import com.twitter.stitch.Stitch - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class UserLocationFetcher @Inject() ( - locationServiceClient: LocationServiceClient, - reverseGeocodeClient: ReverseGeocodeClient, - statsReceiver: StatsReceiver) { - - private val stats: StatsReceiver = statsReceiver.scope("user_location_fetcher") - private val totalRequestsCounter = stats.counter("requests") - private val emptyResponsesCounter = stats.counter("empty") - private val locationServiceExceptionCounter = stats.counter("location_service_exception") - private val reverseGeocodeExceptionCounter = stats.counter("reverse_geocode_exception") - - def getGeohashAndCountryCode( - userId: Option[Long], - ipAddress: Option[String] - ): Stitch[Option[GeohashAndCountryCode]] = { - totalRequestsCounter.incr() - val lscLocationStitch = Stitch - .collect { - userId.map(locationServiceClient.getGeohashAndCountryCode) - }.rescue { - case _: Exception => - locationServiceExceptionCounter.incr() - Stitch.None - } - - val ipLocationStitch = Stitch - .collect { - ipAddress.map(reverseGeocodeClient.getGeohashAndCountryCode) - }.rescue { - case _: Exception => - reverseGeocodeExceptionCounter.incr() - Stitch.None - } - - Stitch.join(lscLocationStitch, ipLocationStitch).map { - case (lscLocation, ipLocation) => { - val geohash = lscLocation.flatMap(_.geohash).orElse(ipLocation.flatMap(_.geohash)) - val countryCode = - lscLocation.flatMap(_.countryCode).orElse(ipLocation.flatMap(_.countryCode)) - (geohash, countryCode) match { - case (None, None) => - emptyResponsesCounter.incr() - None - case _ => Some(GeohashAndCountryCode(geohash, countryCode)) - } - } - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/BUILD deleted file mode 100644 index 77cb553c0..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/BUILD +++ /dev/null @@ -1,21 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/github/nscala_time", - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "finatra/inject/inject-thrift-client/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common", - "src/thrift/com/twitter/gizmoduck:thrift-scala", - "stitch/stitch-gizmoduck", - "util/util-core:scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/GizmoduckClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/GizmoduckClient.scala deleted file mode 100644 index 25b5f0d75..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/GizmoduckClient.scala +++ /dev/null @@ -1,81 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.gizmoduck - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.gizmoduck.thriftscala.LookupContext -import com.twitter.gizmoduck.thriftscala.PerspectiveEdge -import com.twitter.gizmoduck.thriftscala.QueryFields -import com.twitter.stitch.Stitch -import com.twitter.stitch.gizmoduck.Gizmoduck -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class GizmoduckClient @Inject() (gizmoduckStitchClient: Gizmoduck, statsReceiver: StatsReceiver) { - val stats = statsReceiver.scope("gizmoduck_client") - val getByIdStats = stats.scope("get_by_id") - val getUserById = stats.scope("get_user_by_id") - - def isProtected(userId: Long): Stitch[Boolean] = { - // get latency metrics with StatsUtil.profileStitch when calling .getById - val response = StatsUtil.profileStitch( - gizmoduckStitchClient.getById(userId, Set(QueryFields.Safety)), - getByIdStats - ) - response.map { result => - result.user.flatMap(_.safety).map(_.isProtected).getOrElse(true) - } - } - - def getUserName(userId: Long, forUserId: Long): Stitch[Option[String]] = { - val queryFields = GizmoduckClient.GetUserByIdUserNameQueryFields - val lookupContext = LookupContext( - forUserId = Some(forUserId), - perspectiveEdges = Some(GizmoduckClient.DefaultPerspectiveEdges) - ) - // get latency metrics with StatsUtil.profileStitch when calling .getUserById - val response = StatsUtil.profileStitch( - gizmoduckStitchClient.getUserById(userId, queryFields, lookupContext), - getUserById - ) - response.map(_.profile.map(_.name)) - } -} - -object GizmoduckClient { - // Similar to GizmoduckUserRepository.DefaultPerspectiveEdges - val DefaultPerspectiveEdges: Set[PerspectiveEdge] = - Set( - PerspectiveEdge.Blocking, - PerspectiveEdge.BlockedBy, - PerspectiveEdge.DeviceFollowing, - PerspectiveEdge.FollowRequestSent, - PerspectiveEdge.Following, - PerspectiveEdge.FollowedBy, - PerspectiveEdge.LifelineFollowing, - PerspectiveEdge.LifelineFollowedBy, - PerspectiveEdge.Muting, - PerspectiveEdge.NoRetweetsFrom - ) - - // From GizmoduckUserRepository.DefaultQueryFields - val GetUserByIdQueryFields: Set[QueryFields] = Set( - QueryFields.Account, - QueryFields.Counts, - QueryFields.ExtendedProfile, - QueryFields.Perspective, - QueryFields.Profile, - QueryFields.ProfileDesign, - QueryFields.ProfileLocation, - QueryFields.Safety, - QueryFields.Roles, - QueryFields.Takedowns, - QueryFields.UrlEntities, - QueryFields.DirectMessageView, - QueryFields.MediaView - ) - - val GetUserByIdUserNameQueryFields: Set[QueryFields] = Set( - QueryFields.Profile - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/GizmoduckModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/GizmoduckModule.scala deleted file mode 100644 index 9a5efea06..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck/GizmoduckModule.scala +++ /dev/null @@ -1,24 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.gizmoduck - -import com.google.inject.Provides -import com.twitter.finatra.mtls.thriftmux.modules.MtlsClient -import com.twitter.follow_recommendations.common.clients.common.BaseClientModule -import com.twitter.gizmoduck.thriftscala.QueryFields -import com.twitter.gizmoduck.thriftscala.UserService -import com.twitter.stitch.gizmoduck.Gizmoduck -import javax.inject.Singleton - -object GizmoduckModule extends BaseClientModule[UserService.MethodPerEndpoint] with MtlsClient { - override val label = "gizmoduck" - override val dest = "/s/gizmoduck/gizmoduck" - - @Provides - @Singleton - def provideExtraGizmoduckQueryFields: Set[QueryFields] = Set.empty - - @Provides - @Singleton - def providesStitchClient(futureIface: UserService.MethodPerEndpoint): Gizmoduck = { - Gizmoduck(futureIface) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/BUILD deleted file mode 100644 index ec1139cfe..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/inject/inject-thrift-client", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "graph-feature-service/src/main/thrift/com/twitter/graph_feature_service:graph_feature_service_thrift-scala", - "stitch/stitch-core", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/GraphFeatureServiceClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/GraphFeatureServiceClient.scala deleted file mode 100644 index f333d895f..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/GraphFeatureServiceClient.scala +++ /dev/null @@ -1,50 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.graph_feature_service - -import com.twitter.follow_recommendations.common.models.FollowProof -import com.twitter.graph_feature_service.thriftscala.PresetFeatureTypes.WtfTwoHop -import com.twitter.graph_feature_service.thriftscala.EdgeType -import com.twitter.graph_feature_service.thriftscala.GfsIntersectionResponse -import com.twitter.graph_feature_service.thriftscala.GfsPresetIntersectionRequest -import com.twitter.graph_feature_service.thriftscala.{Server => GraphFeatureService} -import com.twitter.stitch.Stitch -import javax.inject.{Inject, Singleton} - -@Singleton -class GraphFeatureServiceClient @Inject() ( - graphFeatureService: GraphFeatureService.MethodPerEndpoint) { - - import GraphFeatureServiceClient._ - def getIntersections( - userId: Long, - candidateIds: Seq[Long], - numIntersectionIds: Int - ): Stitch[Map[Long, FollowProof]] = { - Stitch - .callFuture( - graphFeatureService.getPresetIntersection( - GfsPresetIntersectionRequest(userId, candidateIds, WtfTwoHop, Some(numIntersectionIds)) - ) - ).map { - case GfsIntersectionResponse(gfsIntersectionResults) => - (for { - candidateId <- candidateIds - gfsIntersectionResultForCandidate = - gfsIntersectionResults.filter(_.candidateUserId == candidateId) - followProof <- for { - result <- gfsIntersectionResultForCandidate - intersection <- result.intersectionValues - if leftEdgeTypes.contains(intersection.featureType.leftEdgeType) - if rightEdgeTypes.contains(intersection.featureType.rightEdgeType) - intersectionIds <- intersection.intersectionIds.toSeq - } yield FollowProof(intersectionIds, intersection.count.getOrElse(0)) - } yield { - candidateId -> followProof - }).toMap - } - } -} - -object GraphFeatureServiceClient { - val leftEdgeTypes: Set[EdgeType] = Set(EdgeType.Following) - val rightEdgeTypes: Set[EdgeType] = Set(EdgeType.FollowedBy) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/GraphFeatureStoreModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/GraphFeatureStoreModule.scala deleted file mode 100644 index 8d15358cd..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service/GraphFeatureStoreModule.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.graph_feature_service - -import com.twitter.finatra.mtls.thriftmux.modules.MtlsClient -import com.twitter.follow_recommendations.common.clients.common.BaseClientModule -import com.twitter.graph_feature_service.thriftscala.{Server => GraphFeatureService} - -object GraphFeatureStoreModule - extends BaseClientModule[GraphFeatureService.MethodPerEndpoint] - with MtlsClient { - override val label = "graph_feature_service" - override val dest = "/s/cassowary/graph_feature_service-server" -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/BUILD deleted file mode 100644 index 7432ce040..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/BUILD +++ /dev/null @@ -1,18 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/github/nscala_time", - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/thrift/src/main/thrift:thrift-scala", - "stitch/stitch-socialgraph", - "util/util-core:scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/ImpressionStoreModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/ImpressionStoreModule.scala deleted file mode 100644 index 35b5d7885..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/ImpressionStoreModule.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.impression_store - -import com.google.inject.Provides -import com.google.inject.Singleton -import com.twitter.follow_recommendations.thriftscala.DisplayLocation -import com.twitter.inject.TwitterModule -import com.twitter.strato.catalog.Scan.Slice -import com.twitter.strato.client.Client -import com.twitter.strato.thrift.ScroogeConvImplicits._ - -object ImpressionStoreModule extends TwitterModule { - - val columnPath: String = "onboarding/userrecs/wtfImpressionCountsStore" - - type PKey = (Long, DisplayLocation) - type LKey = Long - type Value = (Long, Int) - - @Provides - @Singleton - def providesImpressionStore(stratoClient: Client): WtfImpressionStore = { - new WtfImpressionStore( - stratoClient.scanner[ - (PKey, Slice[LKey]), - Unit, - (PKey, LKey), - Value - ](columnPath) - ) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/WtfImpressionStore.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/WtfImpressionStore.scala deleted file mode 100644 index 68692dc28..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store/WtfImpressionStore.scala +++ /dev/null @@ -1,42 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.impression_store - -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.common.models.WtfImpression -import com.twitter.follow_recommendations.thriftscala.{DisplayLocation => TDisplayLocation} -import com.twitter.stitch.Stitch -import com.twitter.strato.catalog.Scan.Slice -import com.twitter.strato.client.Scanner -import com.twitter.util.Time -import com.twitter.util.logging.Logging -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class WtfImpressionStore @Inject() ( - scanner: Scanner[ - ((Long, TDisplayLocation), Slice[Long]), - Unit, - ((Long, TDisplayLocation), Long), - (Long, Int) - ]) extends Logging { - def get(userId: Long, dl: DisplayLocation): Stitch[Seq[WtfImpression]] = { - val thriftDl = dl.toThrift - scanner.scan(((userId, thriftDl), Slice.all[Long])).map { impressionsPerDl => - val wtfImpressions = - for { - (((_, _), candidateId), (latestTs, counts)) <- impressionsPerDl - } yield WtfImpression( - candidateId = candidateId, - displayLocation = dl, - latestTime = Time.fromMilliseconds(latestTs), - counts = counts - ) - wtfImpressions - } rescue { - // fail open so that the request can still go through - case ex: Throwable => - logger.warn(s"$dl WtfImpressionsStore warn: " + ex.getMessage) - Stitch.Nil - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/interests_service/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/interests_service/BUILD deleted file mode 100644 index 0835d840b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/interests_service/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -scala_library( - name = "interests_service", - sources = ["InterestServiceClient.scala"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "frigate/frigate-common/src/main/scala/com/twitter/frigate/common/store/interests", - "interests-service/thrift/src/main/thrift:thrift-scala", - "strato/src/main/scala/com/twitter/strato/catalog", - "strato/src/main/scala/com/twitter/strato/client", - "strato/src/main/scala/com/twitter/strato/data", - "strato/src/main/scala/com/twitter/strato/thrift", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/interests_service/InterestServiceClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/interests_service/InterestServiceClient.scala deleted file mode 100644 index 3904cf515..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/interests_service/InterestServiceClient.scala +++ /dev/null @@ -1,115 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.interests_service - -import com.google.inject.Inject -import com.google.inject.Singleton -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.frigate.common.store.InterestedInInterestsFetchKey -import com.twitter.inject.Logging -import com.twitter.interests.thriftscala.InterestId -import com.twitter.interests.thriftscala.InterestRelationship -import com.twitter.interests.thriftscala.InterestedInInterestModel -import com.twitter.interests.thriftscala.UserInterest -import com.twitter.interests.thriftscala.UserInterestData -import com.twitter.interests.thriftscala.UserInterestsResponse -import com.twitter.stitch.Stitch -import com.twitter.strato.client.Client -import com.twitter.strato.thrift.ScroogeConvImplicits._ - -@Singleton -class InterestServiceClient @Inject() ( - stratoClient: Client, - statsReceiver: StatsReceiver = NullStatsReceiver) - extends Logging { - - val interestsServiceStratoColumnPath = "interests/interestedInInterests" - val stats = statsReceiver.scope("interest_service_client") - val errorCounter = stats.counter("error") - - private val interestsFetcher = - stratoClient.fetcher[InterestedInInterestsFetchKey, UserInterestsResponse]( - interestsServiceStratoColumnPath, - checkTypes = true - ) - - def fetchUttInterestIds( - userId: Long - ): Stitch[Seq[Long]] = { - fetchInterestRelationships(userId) - .map(_.toSeq.flatten.flatMap(extractUttInterest)) - } - - def extractUttInterest( - interestRelationShip: InterestRelationship - ): Option[Long] = { - interestRelationShip match { - case InterestRelationship.V1(relationshipV1) => - relationshipV1.interestId match { - case InterestId.SemanticCore(semanticCoreInterest) => Some(semanticCoreInterest.id) - case _ => None - } - case _ => None - } - } - - def fetchCustomInterests( - userId: Long - ): Stitch[Seq[String]] = { - fetchInterestRelationships(userId) - .map(_.toSeq.flatten.flatMap(extractCustomInterest)) - } - - def extractCustomInterest( - interestRelationShip: InterestRelationship - ): Option[String] = { - interestRelationShip match { - case InterestRelationship.V1(relationshipV1) => - relationshipV1.interestId match { - case InterestId.FreeForm(freeFormInterest) => Some(freeFormInterest.interest) - case _ => None - } - case _ => None - } - } - - def fetchInterestRelationships( - userId: Long - ): Stitch[Option[Seq[InterestRelationship]]] = { - interestsFetcher - .fetch( - InterestedInInterestsFetchKey( - userId = userId, - labels = None, - None - )) - .map(_.v) - .map { - case Some(response) => - response.interests.interests.map { interests => - interests.collect { - case UserInterest(_, Some(interestData)) => - getInterestRelationship(interestData) - }.flatten - } - case _ => None - } - .rescue { - case e: Throwable => // we are swallowing all errors - logger.warn(s"interests could not be retrieved for user $userId due to ${e.getCause}") - errorCounter.incr - Stitch.None - } - } - - private def getInterestRelationship( - interestData: UserInterestData - ): Seq[InterestRelationship] = { - interestData match { - case UserInterestData.InterestedIn(interestModels) => - interestModels.collect { - case InterestedInInterestModel.ExplicitModel(model) => model - } - case _ => Nil - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/BUILD deleted file mode 100644 index 37a7f5906..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/inject/inject-thrift-client", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "phonestorage/server/src/main/thrift/com/twitter/phonestorage/api:phone-storage-service-scala", - "stitch/stitch-core", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/PhoneStorageServiceClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/PhoneStorageServiceClient.scala deleted file mode 100644 index 46e76b162..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/PhoneStorageServiceClient.scala +++ /dev/null @@ -1,34 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.phone_storage_service - -import com.twitter.cds.contact_consent_state.thriftscala.PurposeOfProcessing -import com.twitter.phonestorage.api.thriftscala.GetUserPhonesByUsersRequest -import com.twitter.phonestorage.api.thriftscala.PhoneStorageService -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PhoneStorageServiceClient @Inject() ( - val phoneStorageService: PhoneStorageService.MethodPerEndpoint) { - - /** - * PSS can potentially return multiple phone records. - * The current implementation of getUserPhonesByUsers returns only a single phone for a single user_id but - * we can trivially support handling multiple in case that changes in the future. - */ - def getPhoneNumbers( - userId: Long, - purposeOfProcessing: PurposeOfProcessing, - forceCarrierLookup: Option[Boolean] = None - ): Stitch[Seq[String]] = { - val req = GetUserPhonesByUsersRequest( - userIds = Seq(userId), - forceCarrierLookup = forceCarrierLookup, - purposesOfProcessing = Some(Seq(purposeOfProcessing)) - ) - - Stitch.callFuture(phoneStorageService.getUserPhonesByUsers(req)) map { - _.userPhones.map(_.phoneNumber) - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/PhoneStorageServiceModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/PhoneStorageServiceModule.scala deleted file mode 100644 index 90767a509..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service/PhoneStorageServiceModule.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.phone_storage_service - -import com.twitter.finatra.mtls.thriftmux.modules.MtlsClient -import com.twitter.follow_recommendations.common.clients.common.BaseClientModule -import com.twitter.phonestorage.api.thriftscala.PhoneStorageService - -object PhoneStorageServiceModule - extends BaseClientModule[PhoneStorageService.MethodPerEndpoint] - with MtlsClient { - override val label = "phone-storage-service" - override val dest = "/s/ibis-ds-api/ibis-ds-api:thrift2" -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/BUILD deleted file mode 100644 index f711bc3e7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "strato/config/columns/ml/featureStore:featureStore-strato-client", - "strato/config/columns/onboarding/userrecs:userrecs-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/Engagement.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/Engagement.scala deleted file mode 100644 index 98c0555e7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/Engagement.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.real_time_real_graph - -sealed trait EngagementType - -// We do not include SoftFollow since it's deprecated -object EngagementType { - object Click extends EngagementType - object Like extends EngagementType - object Mention extends EngagementType - object Retweet extends EngagementType - object ProfileView extends EngagementType -} - -case class Engagement(engagementType: EngagementType, timestamp: Long) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/EngagementScorer.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/EngagementScorer.scala deleted file mode 100644 index c41fcc98e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/EngagementScorer.scala +++ /dev/null @@ -1,58 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.real_time_real_graph - -import com.twitter.conversions.DurationOps._ -import com.twitter.util.Time - -object EngagementScorer { - private[real_time_real_graph] val MemoryDecayHalfLife = 24.hour - private val ScoringFunctionBase = 0.5 - - def apply( - engagements: Map[Long, Seq[Engagement]], - engagementScoreMap: Map[EngagementType, Double], - minScore: Double = 0.0 - ): Seq[(Long, Double, Seq[EngagementType])] = { - val now = Time.now - engagements - .mapValues { engags => - val totalScore = engags.map { engagement => score(engagement, now, engagementScoreMap) }.sum - val engagementProof = getEngagementProof(engags, engagementScoreMap) - (totalScore, engagementProof) - } - .collect { case (uid, (score, proof)) if score > minScore => (uid, score, proof) } - .toSeq - .sortBy(-_._2) - } - - /** - * The engagement score is the base score decayed via timestamp, loosely model the human memory forgetting - * curve, see https://en.wikipedia.org/wiki/Forgetting_curve - */ - private[real_time_real_graph] def score( - engagement: Engagement, - now: Time, - engagementScoreMap: Map[EngagementType, Double] - ): Double = { - val timeLapse = math.max(now.inMillis - engagement.timestamp, 0) - val engagementScore = engagementScoreMap.getOrElse(engagement.engagementType, 0.0) - engagementScore * math.pow( - ScoringFunctionBase, - timeLapse.toDouble / MemoryDecayHalfLife.inMillis) - } - - private def getEngagementProof( - engagements: Seq[Engagement], - engagementScoreMap: Map[EngagementType, Double] - ): Seq[EngagementType] = { - - val filteredEngagement = engagements - .collectFirst { - case engagement - if engagement.engagementType != EngagementType.Click - && engagementScoreMap.get(engagement.engagementType).exists(_ > 0.0) => - engagement.engagementType - } - - Seq(filteredEngagement.getOrElse(EngagementType.Click)) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/RealTimeRealGraphClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/RealTimeRealGraphClient.scala deleted file mode 100644 index 16fedc8a0..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/real_time_real_graph/RealTimeRealGraphClient.scala +++ /dev/null @@ -1,128 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.real_time_real_graph - -import com.google.inject.Inject -import com.google.inject.Singleton -import com.twitter.conversions.DurationOps._ -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.snowflake.id.SnowflakeId -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.ml.featureStore.TimelinesUserVertexOnUserClientColumn -import com.twitter.strato.generated.client.onboarding.userrecs.RealGraphScoresMhOnUserClientColumn -import com.twitter.util.Duration -import com.twitter.util.Time -import com.twitter.wtf.real_time_interaction_graph.thriftscala._ - -@Singleton -class RealTimeRealGraphClient @Inject() ( - timelinesUserVertexOnUserClientColumn: TimelinesUserVertexOnUserClientColumn, - realGraphScoresMhOnUserClientColumn: RealGraphScoresMhOnUserClientColumn) { - - def mapUserVertexToEngagementAndFilter(userVertex: UserVertex): Map[Long, Seq[Engagement]] = { - val minTimestamp = (Time.now - RealTimeRealGraphClient.MaxEngagementAge).inMillis - userVertex.outgoingInteractionMap.mapValues { interactions => - interactions - .flatMap { interaction => RealTimeRealGraphClient.toEngagement(interaction) }.filter( - _.timestamp >= minTimestamp) - }.toMap - } - - def getRecentProfileViewEngagements(userId: Long): Stitch[Map[Long, Seq[Engagement]]] = { - timelinesUserVertexOnUserClientColumn.fetcher - .fetch(userId).map(_.v).map { input => - input - .map { userVertex => - val targetToEngagements = mapUserVertexToEngagementAndFilter(userVertex) - targetToEngagements.mapValues { engagements => - engagements.filter(engagement => - engagement.engagementType == EngagementType.ProfileView) - } - }.getOrElse(Map.empty) - } - } - - def getUsersRecentlyEngagedWith( - userId: Long, - engagementScoreMap: Map[EngagementType, Double], - includeDirectFollowCandidates: Boolean, - includeNonDirectFollowCandidates: Boolean - ): Stitch[Seq[CandidateUser]] = { - val isNewUser = - SnowflakeId.timeFromIdOpt(userId).exists { signupTime => - (Time.now - signupTime) < RealTimeRealGraphClient.MaxNewUserAge - } - val updatedEngagementScoreMap = - if (isNewUser) - engagementScoreMap + (EngagementType.ProfileView -> RealTimeRealGraphClient.ProfileViewScore) - else engagementScoreMap - - Stitch - .join( - timelinesUserVertexOnUserClientColumn.fetcher.fetch(userId).map(_.v), - realGraphScoresMhOnUserClientColumn.fetcher.fetch(userId).map(_.v)).map { - case (Some(userVertex), Some(neighbors)) => - val engagements = mapUserVertexToEngagementAndFilter(userVertex) - - val candidatesAndScores: Seq[(Long, Double, Seq[EngagementType])] = - EngagementScorer.apply(engagements, engagementScoreMap = updatedEngagementScoreMap) - - val directNeighbors = neighbors.candidates.map(_._1).toSet - val (directFollows, nonDirectFollows) = candidatesAndScores - .partition { - case (id, _, _) => directNeighbors.contains(id) - } - - val candidates = - (if (includeNonDirectFollowCandidates) nonDirectFollows else Seq.empty) ++ - (if (includeDirectFollowCandidates) - directFollows.take(RealTimeRealGraphClient.MaxNumDirectFollow) - else Seq.empty) - - candidates.map { - case (id, score, proof) => - CandidateUser(id, Some(score)) - } - - case _ => Nil - } - } - - def getRealGraphWeights(userId: Long): Stitch[Map[Long, Double]] = - realGraphScoresMhOnUserClientColumn.fetcher - .fetch(userId) - .map( - _.v - .map(_.candidates.map(candidate => (candidate.userId, candidate.score)).toMap) - .getOrElse(Map.empty[Long, Double])) -} - -object RealTimeRealGraphClient { - private def toEngagement(interaction: Interaction): Option[Engagement] = { - // We do not include SoftFollow since it's deprecated - interaction match { - case Interaction.Retweet(Retweet(timestamp)) => - Some(Engagement(EngagementType.Retweet, timestamp)) - case Interaction.Favorite(Favorite(timestamp)) => - Some(Engagement(EngagementType.Like, timestamp)) - case Interaction.Click(Click(timestamp)) => Some(Engagement(EngagementType.Click, timestamp)) - case Interaction.Mention(Mention(timestamp)) => - Some(Engagement(EngagementType.Mention, timestamp)) - case Interaction.ProfileView(ProfileView(timestamp)) => - Some(Engagement(EngagementType.ProfileView, timestamp)) - case _ => None - } - } - - val MaxNumDirectFollow = 50 - val MaxEngagementAge: Duration = 14.days - val MaxNewUserAge: Duration = 30.days - val ProfileViewScore = 0.4 - val EngagementScoreMap = Map( - EngagementType.Like -> 1.0, - EngagementType.Retweet -> 1.0, - EngagementType.Mention -> 1.0 - ) - val StrongEngagementScoreMap = Map( - EngagementType.Like -> 1.0, - EngagementType.Retweet -> 1.0, - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/BUILD deleted file mode 100644 index 58b5001fd..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/BUILD +++ /dev/null @@ -1,26 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/github/nscala_time", - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "escherbird/src/scala/com/twitter/escherbird/util/stitchcache", - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "finatra/inject/inject-thrift-client/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "socialgraph/server/src/main/scala/com/twitter/socialgraph/util", - "src/thrift/com/twitter/socialgraph:thrift-scala", - "stitch/stitch-socialgraph", - "strato/config/columns/onboarding/socialGraphService:socialGraphService-strato-client", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-core:scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/SocialGraphClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/SocialGraphClient.scala deleted file mode 100644 index 3ad90b5ed..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/SocialGraphClient.scala +++ /dev/null @@ -1,421 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.socialgraph - -import com.twitter.escherbird.util.stitchcache.StitchCache -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.follow_recommendations.common.models.FollowProof -import com.twitter.follow_recommendations.common.models.UserIdWithTimestamp -import com.twitter.inject.Logging -import com.twitter.socialgraph.thriftscala.EdgesRequest -import com.twitter.socialgraph.thriftscala.IdsRequest -import com.twitter.socialgraph.thriftscala.IdsResult -import com.twitter.socialgraph.thriftscala.LookupContext -import com.twitter.socialgraph.thriftscala.OverCapacity -import com.twitter.socialgraph.thriftscala.PageRequest -import com.twitter.socialgraph.thriftscala.RelationshipType -import com.twitter.socialgraph.thriftscala.SrcRelationship -import com.twitter.socialgraph.util.ByteBufferUtil -import com.twitter.stitch.Stitch -import com.twitter.stitch.socialgraph.SocialGraph -import com.twitter.strato.client.Fetcher -import com.twitter.strato.generated.client.onboarding.socialGraphService.IdsClientColumn -import com.twitter.util.Duration -import com.twitter.util.Time -import java.nio.ByteBuffer -import javax.inject.Inject -import javax.inject.Singleton - -case class RecentEdgesQuery( - userId: Long, - relations: Seq[RelationshipType], - // prefer to default value to better utilize the caching function of stitch - count: Option[Int] = Some(SocialGraphClient.MaxQuerySize), - performUnion: Boolean = true, - recentEdgesWindowOpt: Option[Duration] = None, - targets: Option[Seq[Long]] = None) - -case class EdgeRequestQuery( - userId: Long, - relation: RelationshipType, - count: Option[Int] = Some(SocialGraphClient.MaxQuerySize), - performUnion: Boolean = true, - recentEdgesWindowOpt: Option[Duration] = None, - targets: Option[Seq[Long]] = None) - -@Singleton -class SocialGraphClient @Inject() ( - socialGraph: SocialGraph, - idsClientColumn: IdsClientColumn, - statsReceiver: StatsReceiver = NullStatsReceiver) - extends Logging { - - private val stats = statsReceiver.scope(this.getClass.getSimpleName) - private val cacheStats = stats.scope("cache") - private val getIntersectionsStats = stats.scope("getIntersections") - private val getIntersectionsFromCachedColumnStats = - stats.scope("getIntersectionsFromCachedColumn") - private val getRecentEdgesStats = stats.scope("getRecentEdges") - private val getRecentEdgesCachedStats = stats.scope("getRecentEdgesCached") - private val getRecentEdgesFromCachedColumnStats = stats.scope("getRecentEdgesFromCachedColumn") - private val getRecentEdgesCachedInternalStats = stats.scope("getRecentEdgesCachedInternal") - private val getRecentEdgesWithTimeStats = stats.scope("getRecentEdgesWithTime") - - val sgsIdsFetcher: Fetcher[IdsRequest, Unit, IdsResult] = idsClientColumn.fetcher - - private val recentEdgesCache = StitchCache[RecentEdgesQuery, Seq[Long]]( - maxCacheSize = SocialGraphClient.MaxCacheSize, - ttl = SocialGraphClient.CacheTTL, - statsReceiver = cacheStats, - underlyingCall = getRecentEdges - ) - - def getRecentEdgesCached( - rq: RecentEdgesQuery, - useCachedStratoColumn: Boolean = true - ): Stitch[Seq[Long]] = { - getRecentEdgesCachedStats.counter("requests").incr() - if (useCachedStratoColumn) { - getRecentEdgesFromCachedColumn(rq) - } else { - StatsUtil.profileStitch( - getRecentEdgesCachedInternal(rq), - getRecentEdgesCachedInternalStats - ) - } - } - - def getRecentEdgesCachedInternal(rq: RecentEdgesQuery): Stitch[Seq[Long]] = { - recentEdgesCache.readThrough(rq) - } - - def getRecentEdgesFromCachedColumn(rq: RecentEdgesQuery): Stitch[Seq[Long]] = { - val pageRequest = rq.recentEdgesWindowOpt match { - case Some(recentEdgesWindow) => - PageRequest( - count = rq.count, - cursor = Some(getEdgeCursor(recentEdgesWindow)), - selectAll = Some(true) - ) - case _ => PageRequest(count = rq.count) - } - val idsRequest = IdsRequest( - rq.relations.map { relationshipType => - SrcRelationship( - source = rq.userId, - relationshipType = relationshipType, - targets = rq.targets - ) - }, - pageRequest = Some(pageRequest), - context = Some(LookupContext(performUnion = Some(rq.performUnion))) - ) - - val socialGraphStitch = sgsIdsFetcher - .fetch(idsRequest, Unit) - .map(_.v) - .map { result => - result - .map { idResult => - val userIds: Seq[Long] = idResult.ids - getRecentEdgesFromCachedColumnStats.stat("num_edges").add(userIds.size) - userIds - }.getOrElse(Seq.empty) - } - .rescue { - case e: Exception => - stats.counter(e.getClass.getSimpleName).incr() - Stitch.Nil - } - - StatsUtil.profileStitch( - socialGraphStitch, - getRecentEdgesFromCachedColumnStats - ) - } - - def getRecentEdges(rq: RecentEdgesQuery): Stitch[Seq[Long]] = { - val pageRequest = rq.recentEdgesWindowOpt match { - case Some(recentEdgesWindow) => - PageRequest( - count = rq.count, - cursor = Some(getEdgeCursor(recentEdgesWindow)), - selectAll = Some(true) - ) - case _ => PageRequest(count = rq.count) - } - val socialGraphStitch = socialGraph - .ids( - IdsRequest( - rq.relations.map { relationshipType => - SrcRelationship( - source = rq.userId, - relationshipType = relationshipType, - targets = rq.targets - ) - }, - pageRequest = Some(pageRequest), - context = Some(LookupContext(performUnion = Some(rq.performUnion))) - ) - ) - .map { idsResult => - val userIds: Seq[Long] = idsResult.ids - getRecentEdgesStats.stat("num_edges").add(userIds.size) - userIds - } - .rescue { - case e: OverCapacity => - stats.counter(e.getClass.getSimpleName).incr() - logger.warn("SGS Over Capacity", e) - Stitch.Nil - } - StatsUtil.profileStitch( - socialGraphStitch, - getRecentEdgesStats - ) - } - - // This method return recent edges of (userId, timeInMs) - def getRecentEdgesWithTime(rq: EdgeRequestQuery): Stitch[Seq[UserIdWithTimestamp]] = { - val pageRequest = rq.recentEdgesWindowOpt match { - case Some(recentEdgesWindow) => - PageRequest( - count = rq.count, - cursor = Some(getEdgeCursor(recentEdgesWindow)), - selectAll = Some(true) - ) - case _ => PageRequest(count = rq.count) - } - - val socialGraphStitch = socialGraph - .edges( - EdgesRequest( - SrcRelationship( - source = rq.userId, - relationshipType = rq.relation, - targets = rq.targets - ), - pageRequest = Some(pageRequest), - context = Some(LookupContext(performUnion = Some(rq.performUnion))) - ) - ) - .map { edgesResult => - val userIds = edgesResult.edges.map { socialEdge => - UserIdWithTimestamp(socialEdge.target, socialEdge.updatedAt) - } - getRecentEdgesWithTimeStats.stat("num_edges").add(userIds.size) - userIds - } - .rescue { - case e: OverCapacity => - stats.counter(e.getClass.getSimpleName).incr() - logger.warn("SGS Over Capacity", e) - Stitch.Nil - } - StatsUtil.profileStitch( - socialGraphStitch, - getRecentEdgesWithTimeStats - ) - } - - // This method returns the cursor for a time duration, such that all the edges returned by SGS will be created - // in the range (now-window, now) - def getEdgeCursor(window: Duration): ByteBuffer = { - val cursorInLong = (-(Time.now - window).inMilliseconds) << 20 - ByteBufferUtil.fromLong(cursorInLong) - } - - // notice that this is more expensive but more realtime than the GFS one - def getIntersections( - userId: Long, - candidateIds: Seq[Long], - numIntersectionIds: Int - ): Stitch[Map[Long, FollowProof]] = { - val socialGraphStitch: Stitch[Map[Long, FollowProof]] = Stitch - .collect(candidateIds.map { candidateId => - socialGraph - .ids( - IdsRequest( - Seq( - SrcRelationship(userId, RelationshipType.Following), - SrcRelationship(candidateId, RelationshipType.FollowedBy) - ), - pageRequest = Some(PageRequest(count = Some(numIntersectionIds))) - ) - ).map { idsResult => - getIntersectionsStats.stat("num_edges").add(idsResult.ids.size) - (candidateId -> FollowProof(idsResult.ids, idsResult.ids.size)) - } - }).map(_.toMap) - .rescue { - case e: OverCapacity => - stats.counter(e.getClass.getSimpleName).incr() - logger.warn("social graph over capacity in hydrating social proof", e) - Stitch.value(Map.empty) - } - StatsUtil.profileStitch( - socialGraphStitch, - getIntersectionsStats - ) - } - - def getIntersectionsFromCachedColumn( - userId: Long, - candidateIds: Seq[Long], - numIntersectionIds: Int - ): Stitch[Map[Long, FollowProof]] = { - val socialGraphStitch: Stitch[Map[Long, FollowProof]] = Stitch - .collect(candidateIds.map { candidateId => - val idsRequest = IdsRequest( - Seq( - SrcRelationship(userId, RelationshipType.Following), - SrcRelationship(candidateId, RelationshipType.FollowedBy) - ), - pageRequest = Some(PageRequest(count = Some(numIntersectionIds))) - ) - - sgsIdsFetcher - .fetch(idsRequest, Unit) - .map(_.v) - .map { resultOpt => - resultOpt.map { idsResult => - getIntersectionsFromCachedColumnStats.stat("num_edges").add(idsResult.ids.size) - candidateId -> FollowProof(idsResult.ids, idsResult.ids.size) - } - } - }).map(_.flatten.toMap) - .rescue { - case e: Exception => - stats.counter(e.getClass.getSimpleName).incr() - Stitch.value(Map.empty) - } - StatsUtil.profileStitch( - socialGraphStitch, - getIntersectionsFromCachedColumnStats - ) - } - - def getInvalidRelationshipUserIds( - userId: Long, - maxNumRelationship: Int = SocialGraphClient.MaxNumInvalidRelationship - ): Stitch[Seq[Long]] = { - getRecentEdges( - RecentEdgesQuery( - userId, - SocialGraphClient.InvalidRelationshipTypes, - Some(maxNumRelationship) - ) - ) - } - - def getInvalidRelationshipUserIdsFromCachedColumn( - userId: Long, - maxNumRelationship: Int = SocialGraphClient.MaxNumInvalidRelationship - ): Stitch[Seq[Long]] = { - getRecentEdgesFromCachedColumn( - RecentEdgesQuery( - userId, - SocialGraphClient.InvalidRelationshipTypes, - Some(maxNumRelationship) - ) - ) - } - - def getRecentFollowedUserIds(userId: Long): Stitch[Seq[Long]] = { - getRecentEdges( - RecentEdgesQuery( - userId, - Seq(RelationshipType.Following) - ) - ) - } - - def getRecentFollowedUserIdsFromCachedColumn(userId: Long): Stitch[Seq[Long]] = { - getRecentEdgesFromCachedColumn( - RecentEdgesQuery( - userId, - Seq(RelationshipType.Following) - ) - ) - } - - def getRecentFollowedUserIdsWithTime(userId: Long): Stitch[Seq[UserIdWithTimestamp]] = { - getRecentEdgesWithTime( - EdgeRequestQuery( - userId, - RelationshipType.Following - ) - ) - } - - def getRecentFollowedByUserIds(userId: Long): Stitch[Seq[Long]] = { - getRecentEdges( - RecentEdgesQuery( - userId, - Seq(RelationshipType.FollowedBy) - ) - ) - } - - def getRecentFollowedByUserIdsFromCachedColumn(userId: Long): Stitch[Seq[Long]] = { - getRecentEdgesFromCachedColumn( - RecentEdgesQuery( - userId, - Seq(RelationshipType.FollowedBy) - ) - ) - } - - def getRecentFollowedUserIdsWithTimeWindow( - userId: Long, - timeWindow: Duration - ): Stitch[Seq[Long]] = { - getRecentEdges( - RecentEdgesQuery( - userId, - Seq(RelationshipType.Following), - recentEdgesWindowOpt = Some(timeWindow) - ) - ) - } -} - -object SocialGraphClient { - - val MaxQuerySize: Int = 500 - val MaxCacheSize: Int = 5000000 - // Ref: src/thrift/com/twitter/socialgraph/social_graph_service.thrift - val MaxNumInvalidRelationship: Int = 5000 - val CacheTTL: Duration = Duration.fromHours(24) - - val InvalidRelationshipTypes: Seq[RelationshipType] = Seq( - RelationshipType.HideRecommendations, - RelationshipType.Blocking, - RelationshipType.BlockedBy, - RelationshipType.Muting, - RelationshipType.MutedBy, - RelationshipType.ReportedAsSpam, - RelationshipType.ReportedAsSpamBy, - RelationshipType.ReportedAsAbuse, - RelationshipType.ReportedAsAbuseBy, - RelationshipType.FollowRequestOutgoing, - RelationshipType.Following, - RelationshipType.UsedToFollow, - ) - - /** - * - * Whether to call SGS to validate each candidate based on the number of invalid relationship users - * prefetched during request building step. This aims to not omit any invalid candidates that are - * not filtered out in previous steps. - * If the number is 0, this might be a fail-opened SGS call. - * If the number is larger or equal to 5000, this could hit SGS page size limit. - * Both cases account for a small percentage of the total traffic (<5%). - * - * @param numInvalidRelationshipUsers number of invalid relationship users fetched from getInvalidRelationshipUserIds - * @return whether to enable post-ranker SGS predicate - */ - def enablePostRankerSgsPredicate(numInvalidRelationshipUsers: Int): Boolean = { - numInvalidRelationshipUsers == 0 || numInvalidRelationshipUsers >= MaxNumInvalidRelationship - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/SocialGraphModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/SocialGraphModule.scala deleted file mode 100644 index 5a97448d1..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph/SocialGraphModule.scala +++ /dev/null @@ -1,25 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.socialgraph - -import com.google.inject.Provides -import com.twitter.finagle.ThriftMux -import com.twitter.finatra.mtls.thriftmux.modules.MtlsClient -import com.twitter.follow_recommendations.common.clients.common.BaseClientModule -import com.twitter.socialgraph.thriftscala.SocialGraphService -import com.twitter.stitch.socialgraph.SocialGraph -import javax.inject.Singleton - -object SocialGraphModule - extends BaseClientModule[SocialGraphService.MethodPerEndpoint] - with MtlsClient { - override val label = "social-graph-service" - override val dest = "/s/socialgraph/socialgraph" - - override def configureThriftMuxClient(client: ThriftMux.Client): ThriftMux.Client = - client.withSessionQualifier.noFailFast - - @Provides - @Singleton - def providesStitchClient(futureIface: SocialGraphService.MethodPerEndpoint): SocialGraph = { - SocialGraph(futureIface) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato/BUILD deleted file mode 100644 index 3661887c8..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato/BUILD +++ /dev/null @@ -1,30 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "src/scala/com/twitter/onboarding/relevance/candidate_generation/utt/models", - "src/thrift/com/twitter/core_workflows/user_model:user_model-scala", - "src/thrift/com/twitter/frigate/data_pipeline:frigate-user-history-thrift-scala", - "src/thrift/com/twitter/hermit/candidate:hermit-candidate-scala", - "src/thrift/com/twitter/hermit/pop_geo:hermit-pop-geo-scala", - "src/thrift/com/twitter/onboarding/relevance/relatable_accounts:relatable_accounts-scala", - "src/thrift/com/twitter/onboarding/relevance/store:store-scala", - "src/thrift/com/twitter/recos/user_user_graph:user_user_graph-scala", - "src/thrift/com/twitter/search/account_search/extended_network:extended_network_users-scala", - "src/thrift/com/twitter/service/metastore/gen:thrift-scala", - "src/thrift/com/twitter/wtf/candidate:wtf-candidate-scala", - "src/thrift/com/twitter/wtf/ml:wtf-ml-output-thrift-scala", - "src/thrift/com/twitter/wtf/real_time_interaction_graph:wtf-real_time_interaction_graph-thrift-scala", - "src/thrift/com/twitter/wtf/triangular_loop:triangular_loop-scala", - "strato/src/main/scala/com/twitter/strato/client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato/StratoClientModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato/StratoClientModule.scala deleted file mode 100644 index 4046ac754..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato/StratoClientModule.scala +++ /dev/null @@ -1,249 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.strato - -import com.google.inject.name.Named -import com.google.inject.Provides -import com.google.inject.Singleton -import com.twitter.conversions.DurationOps._ -import com.twitter.core_workflows.user_model.thriftscala.CondensedUserState -import com.twitter.search.account_search.extended_network.thriftscala.ExtendedNetworkUserKey -import com.twitter.search.account_search.extended_network.thriftscala.ExtendedNetworkUserVal -import com.twitter.finagle.ThriftMux -import com.twitter.finagle.mtls.authentication.ServiceIdentifier -import com.twitter.finagle.thrift.Protocols -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.follow_recommendations.common.constants.ServiceConstants._ -import com.twitter.frigate.data_pipeline.candidate_generation.thriftscala.LatestEvents -import com.twitter.hermit.candidate.thriftscala.{Candidates => HermitCandidates} -import com.twitter.hermit.pop_geo.thriftscala.PopUsersInPlace -import com.twitter.onboarding.relevance.relatable_accounts.thriftscala.RelatableAccounts -import com.twitter.inject.TwitterModule -import com.twitter.onboarding.relevance.candidates.thriftscala.InterestBasedUserRecommendations -import com.twitter.onboarding.relevance.candidates.thriftscala.UTTInterest -import com.twitter.onboarding.relevance.store.thriftscala.WhoToFollowDismissEventDetails -import com.twitter.recos.user_user_graph.thriftscala.RecommendUserRequest -import com.twitter.recos.user_user_graph.thriftscala.RecommendUserResponse -import com.twitter.service.metastore.gen.thriftscala.UserRecommendabilityFeatures -import com.twitter.strato.catalog.Scan.Slice -import com.twitter.strato.client.Strato.{Client => StratoClient} -import com.twitter.strato.client.Client -import com.twitter.strato.client.Fetcher -import com.twitter.strato.client.Scanner -import com.twitter.strato.thrift.ScroogeConvImplicits._ -import com.twitter.wtf.candidate.thriftscala.CandidateSeq -import com.twitter.wtf.ml.thriftscala.CandidateFeatures -import com.twitter.wtf.real_time_interaction_graph.thriftscala.Interaction -import com.twitter.wtf.triangular_loop.thriftscala.{Candidates => TriangularLoopCandidates} -import com.twitter.strato.opcontext.Attribution._ - -object StratoClientModule extends TwitterModule { - - // column paths - val CosineFollowPath = "recommendations/similarity/similarUsersByFollowGraph.User" - val CosineListPath = "recommendations/similarity/similarUsersByListGraph.User" - val CuratedCandidatesPath = "onboarding/curatedAccounts" - val CuratedFilteredAccountsPath = "onboarding/filteredAccountsFromRecommendations" - val PopUsersInPlacePath = "onboarding/userrecs/popUsersInPlace" - val ProfileSidebarBlacklistPath = "recommendations/hermit/profile-sidebar-blacklist" - val RealTimeInteractionsPath = "hmli/realTimeInteractions" - val SimsPath = "recommendations/similarity/similarUsersBySims.User" - val DBV2SimsPath = "onboarding/userrecs/newSims.User" - val TriangularLoopsPath = "onboarding/userrecs/triangularLoops.User" - val TwoHopRandomWalkPath = "onboarding/userrecs/twoHopRandomWalk.User" - val UserRecommendabilityPath = "onboarding/userRecommendabilityWithLongKeys.User" - val UTTAccountRecommendationsPath = "onboarding/userrecs/utt_account_recommendations" - val UttSeedAccountsRecommendationPath = "onboarding/userrecs/utt_seed_accounts" - val UserStatePath = "onboarding/userState.User" - val WTFPostNuxFeaturesPath = "ml/featureStore/onboarding/wtfPostNuxFeatures.User" - val ElectionCandidatesPath = "onboarding/electionAccounts" - val UserUserGraphPath = "recommendations/userUserGraph" - val WtfDissmissEventsPath = "onboarding/wtfDismissEvents" - val RelatableAccountsPath = "onboarding/userrecs/relatableAccounts" - val ExtendedNetworkCandidatesPath = "search/account_search/extendedNetworkCandidatesMH" - val LabeledNotificationPath = "frigate/magicrecs/labeledPushRecsAggregated.User" - - @Provides - @Singleton - def stratoClient(serviceIdentifier: ServiceIdentifier): Client = { - val timeoutBudget = 500.milliseconds - StratoClient( - ThriftMux.client - .withRequestTimeout(timeoutBudget) - .withProtocolFactory(Protocols.binaryFactory( - stringLengthLimit = StringLengthLimit, - containerLengthLimit = ContainerLengthLimit))) - .withMutualTls(serviceIdentifier) - .build() - } - - // add strato putters, fetchers, scanners below: - @Provides - @Singleton - @Named(GuiceNamedConstants.COSINE_FOLLOW_FETCHER) - def cosineFollowFetcher(stratoClient: Client): Fetcher[Long, Unit, HermitCandidates] = - stratoClient.fetcher[Long, Unit, HermitCandidates](CosineFollowPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.COSINE_LIST_FETCHER) - def cosineListFetcher(stratoClient: Client): Fetcher[Long, Unit, HermitCandidates] = - stratoClient.fetcher[Long, Unit, HermitCandidates](CosineListPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.CURATED_COMPETITOR_ACCOUNTS_FETCHER) - def curatedBlacklistedAccountsFetcher(stratoClient: Client): Fetcher[String, Unit, Seq[Long]] = - stratoClient.fetcher[String, Unit, Seq[Long]](CuratedFilteredAccountsPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.CURATED_CANDIDATES_FETCHER) - def curatedCandidatesFetcher(stratoClient: Client): Fetcher[String, Unit, Seq[Long]] = - stratoClient.fetcher[String, Unit, Seq[Long]](CuratedCandidatesPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.POP_USERS_IN_PLACE_FETCHER) - def popUsersInPlaceFetcher(stratoClient: Client): Fetcher[String, Unit, PopUsersInPlace] = - stratoClient.fetcher[String, Unit, PopUsersInPlace](PopUsersInPlacePath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.RELATABLE_ACCOUNTS_FETCHER) - def relatableAccountsFetcher(stratoClient: Client): Fetcher[String, Unit, RelatableAccounts] = - stratoClient.fetcher[String, Unit, RelatableAccounts](RelatableAccountsPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.PROFILE_SIDEBAR_BLACKLIST_SCANNER) - def profileSidebarBlacklistScanner( - stratoClient: Client - ): Scanner[(Long, Slice[Long]), Unit, (Long, Long), Unit] = - stratoClient.scanner[(Long, Slice[Long]), Unit, (Long, Long), Unit](ProfileSidebarBlacklistPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.REAL_TIME_INTERACTIONS_FETCHER) - def realTimeInteractionsFetcher( - stratoClient: Client - ): Fetcher[(Long, Long), Unit, Seq[Interaction]] = - stratoClient.fetcher[(Long, Long), Unit, Seq[Interaction]](RealTimeInteractionsPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.SIMS_FETCHER) - def simsFetcher(stratoClient: Client): Fetcher[Long, Unit, HermitCandidates] = - stratoClient.fetcher[Long, Unit, HermitCandidates](SimsPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.DBV2_SIMS_FETCHER) - def dbv2SimsFetcher(stratoClient: Client): Fetcher[Long, Unit, HermitCandidates] = - stratoClient.fetcher[Long, Unit, HermitCandidates](DBV2SimsPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.TRIANGULAR_LOOPS_FETCHER) - def triangularLoopsFetcher(stratoClient: Client): Fetcher[Long, Unit, TriangularLoopCandidates] = - stratoClient.fetcher[Long, Unit, TriangularLoopCandidates](TriangularLoopsPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.TWO_HOP_RANDOM_WALK_FETCHER) - def twoHopRandomWalkFetcher(stratoClient: Client): Fetcher[Long, Unit, CandidateSeq] = - stratoClient.fetcher[Long, Unit, CandidateSeq](TwoHopRandomWalkPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.USER_RECOMMENDABILITY_FETCHER) - def userRecommendabilityFetcher( - stratoClient: Client - ): Fetcher[Long, Unit, UserRecommendabilityFeatures] = - stratoClient.fetcher[Long, Unit, UserRecommendabilityFeatures](UserRecommendabilityPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.USER_STATE_FETCHER) - def userStateFetcher(stratoClient: Client): Fetcher[Long, Unit, CondensedUserState] = - stratoClient.fetcher[Long, Unit, CondensedUserState](UserStatePath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.UTT_ACCOUNT_RECOMMENDATIONS_FETCHER) - def uttAccountRecommendationsFetcher( - stratoClient: Client - ): Fetcher[UTTInterest, Unit, InterestBasedUserRecommendations] = - stratoClient.fetcher[UTTInterest, Unit, InterestBasedUserRecommendations]( - UTTAccountRecommendationsPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.UTT_SEED_ACCOUNTS_FETCHER) - def uttSeedAccountRecommendationsFetcher( - stratoClient: Client - ): Fetcher[UTTInterest, Unit, InterestBasedUserRecommendations] = - stratoClient.fetcher[UTTInterest, Unit, InterestBasedUserRecommendations]( - UttSeedAccountsRecommendationPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.ELECTION_CANDIDATES_FETCHER) - def electionCandidatesFetcher(stratoClient: Client): Fetcher[String, Unit, Seq[Long]] = - stratoClient.fetcher[String, Unit, Seq[Long]](ElectionCandidatesPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.USER_USER_GRAPH_FETCHER) - def userUserGraphFetcher( - stratoClient: Client - ): Fetcher[RecommendUserRequest, Unit, RecommendUserResponse] = - stratoClient.fetcher[RecommendUserRequest, Unit, RecommendUserResponse](UserUserGraphPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.POST_NUX_WTF_FEATURES_FETCHER) - def wtfPostNuxFeaturesFetcher(stratoClient: Client): Fetcher[Long, Unit, CandidateFeatures] = { - val attribution = ManhattanAppId("starbuck", "wtf_starbuck") - stratoClient - .fetcher[Long, Unit, CandidateFeatures](WTFPostNuxFeaturesPath) - .withAttribution(attribution) - } - - @Provides - @Singleton - @Named(GuiceNamedConstants.EXTENDED_NETWORK) - def extendedNetworkFetcher( - stratoClient: Client - ): Fetcher[ExtendedNetworkUserKey, Unit, ExtendedNetworkUserVal] = { - stratoClient - .fetcher[ExtendedNetworkUserKey, Unit, ExtendedNetworkUserVal](ExtendedNetworkCandidatesPath) - } - - @Provides - @Singleton - @Named(GuiceNamedConstants.DISMISS_STORE_SCANNER) - def dismissStoreScanner( - stratoClient: Client - ): Scanner[ - (Long, Slice[(Long, Long)]), - Unit, - (Long, (Long, Long)), - WhoToFollowDismissEventDetails - ] = - stratoClient.scanner[ - (Long, Slice[(Long, Long)]), // PKEY: userId, LKEY: (-ts, candidateId) - Unit, - (Long, (Long, Long)), - WhoToFollowDismissEventDetails - ](WtfDissmissEventsPath) - - @Provides - @Singleton - @Named(GuiceNamedConstants.LABELED_NOTIFICATION_FETCHER) - def labeledNotificationFetcher( - stratoClient: Client - ): Fetcher[Long, Unit, LatestEvents] = { - stratoClient - .fetcher[Long, Unit, LatestEvents](LabeledNotificationPath) - } - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/user_state/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/user_state/BUILD deleted file mode 100644 index 16a82a302..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/user_state/BUILD +++ /dev/null @@ -1,17 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders", - "stitch/stitch-core", - "strato/src/main/scala/com/twitter/strato/client", - "user-signal-service/thrift/src/main/thrift:thrift-scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/user_state/UserStateClient.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/user_state/UserStateClient.scala deleted file mode 100644 index fe8101261..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/user_state/UserStateClient.scala +++ /dev/null @@ -1,83 +0,0 @@ -package com.twitter.follow_recommendations.common.clients.user_state - -import com.google.inject.name.Named -import com.twitter.conversions.DurationOps._ -import com.twitter.core_workflows.user_model.thriftscala.CondensedUserState -import com.twitter.core_workflows.user_model.thriftscala.UserState -import com.twitter.decider.Decider -import com.twitter.decider.RandomRecipient -import com.twitter.finagle.Memcached.Client -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.finagle.util.DefaultTimer -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.follow_recommendations.common.clients.cache.MemcacheClient -import com.twitter.follow_recommendations.common.clients.cache.ThriftEnumOptionBijection -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.follow_recommendations.configapi.deciders.DeciderKey -import com.twitter.stitch.Stitch -import com.twitter.strato.client.Fetcher -import com.twitter.util.Duration -import javax.inject.Inject -import javax.inject.Singleton -import java.lang.{Long => JLong} - -@Singleton -class UserStateClient @Inject() ( - @Named(GuiceNamedConstants.USER_STATE_FETCHER) userStateFetcher: Fetcher[ - Long, - Unit, - CondensedUserState - ], - client: Client, - statsReceiver: StatsReceiver, - decider: Decider = Decider.False) { - - private val stats: StatsReceiver = statsReceiver.scope("user_state_client") - - // client to memcache cluster - val bijection = new ThriftEnumOptionBijection[UserState](UserState.apply) - val memcacheClient = MemcacheClient[Option[UserState]]( - client = client, - dest = "/s/cache/follow_recos_service:twemcaches", - valueBijection = bijection, - ttl = UserStateClient.CacheTTL, - statsReceiver = stats.scope("twemcache") - ) - - def getUserState(userId: Long): Stitch[Option[UserState]] = { - val deciderKey: String = DeciderKey.EnableDistributedCaching.toString - val enableDistributedCaching: Boolean = decider.isAvailable(deciderKey, Some(RandomRecipient)) - val userStateStitch: Stitch[Option[UserState]] = - enableDistributedCaching match { - // read from memcache - case true => memcacheClient.readThrough( - // add a key prefix to address cache key collisions - key = "UserStateClient" + userId.toString, - underlyingCall = () => fetchUserState(userId) - ) - case false => fetchUserState(userId) - } - val userStateStitchWithTimeout: Stitch[Option[UserState]] = - userStateStitch - // set a 150ms timeout limit for user state fetches - .within(150.milliseconds)(DefaultTimer) - .rescue { - case e: Exception => - stats.scope("rescued").counter(e.getClass.getSimpleName).incr() - Stitch(None) - } - // profile the latency of stitch call and return the result - StatsUtil.profileStitch( - userStateStitchWithTimeout, - stats.scope("getUserState") - ) - } - - def fetchUserState(userId: JLong): Stitch[Option[UserState]] = { - userStateFetcher.fetch(userId).map(_.v.flatMap(_.userState)) - } -} - -object UserStateClient { - val CacheTTL: Duration = Duration.fromHours(6) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/BUILD deleted file mode 100644 index dc9335d5b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "util/util-core/src/main/scala/com/twitter/conversions", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/CandidateAlgorithmTypeConstants.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/CandidateAlgorithmTypeConstants.scala deleted file mode 100644 index 7c7892c02..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/CandidateAlgorithmTypeConstants.scala +++ /dev/null @@ -1,91 +0,0 @@ -package com.twitter.follow_recommendations.common.constants - -import com.twitter.hermit.constants.AlgorithmFeedbackTokens.AlgorithmToFeedbackTokenMap -import com.twitter.hermit.model.Algorithm._ -import com.twitter.follow_recommendations.common.models.AlgorithmType - -object CandidateAlgorithmTypeConstants { - - /** - * Each algorithm is based on one, or more, of the 4 types of information we have on users, - * described in [[AlgorithmType]]. Assignment of algorithms to these categories are based on - */ - private val AlgorithmIdToType: Map[String, Set[AlgorithmType.Value]] = Map( - // Activity Algorithms: - AlgorithmToFeedbackTokenMap(NewFollowingSimilarUser).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(Sims).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(NewFollowingSimilarUserSalsa).toString -> Set( - AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(RecentEngagementNonDirectFollow).toString -> Set( - AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(RecentEngagementSimilarUser).toString -> Set( - AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(RecentEngagementSarusOcCur).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(RecentSearchBasedRec).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(TwistlyTweetAuthors).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(Follow2VecNearestNeighbors).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(EmailTweetClick).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(RepeatedProfileVisits).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(GoodTweetClickEngagements).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(TweetShareEngagements).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(TweetSharerToShareRecipientEngagements).toString -> Set( - AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(TweetAuthorToShareRecipientEngagements).toString -> Set( - AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(LinearRegressionFollow2VecNearestNeighbors).toString -> Set( - AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(NUXLOHistory).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(TrafficAttributionAccounts).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(RealGraphOonV2).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(MagicRecsRecentEngagements).toString -> Set(AlgorithmType.Activity), - AlgorithmToFeedbackTokenMap(NotificationEngagement).toString -> Set(AlgorithmType.Activity), - // Social Algorithms: - AlgorithmToFeedbackTokenMap(TwoHopRandomWalk).toString -> Set(AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(RealTimeMutualFollow).toString -> Set(AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(ForwardPhoneBook).toString -> Set(AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(ForwardEmailBook).toString -> Set(AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(NewFollowingNewFollowingExpansion).toString -> Set( - AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(NewFollowingSarusCoOccurSocialProof).toString -> Set( - AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(ReverseEmailBookIbis).toString -> Set(AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(ReversePhoneBook).toString -> Set(AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(StrongTiePredictionRec).toString -> Set(AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(StrongTiePredictionRecWithSocialProof).toString -> Set( - AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(OnlineStrongTiePredictionRec).toString -> Set(AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(OnlineStrongTiePredictionRecNoCaching).toString -> Set( - AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(TriangularLoop).toString -> Set(AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(StrongTiePredictionPmi).toString -> Set(AlgorithmType.Social), - AlgorithmToFeedbackTokenMap(OnlineStrongTiePredictionRAB).toString -> Set(AlgorithmType.Social), - // Geo Algorithms: - AlgorithmToFeedbackTokenMap(PopCountryBackFill).toString -> Set(AlgorithmType.Geo), - AlgorithmToFeedbackTokenMap(PopCountry).toString -> Set(AlgorithmType.Geo), - AlgorithmToFeedbackTokenMap(PopGeohash).toString -> Set(AlgorithmType.Geo), -// AlgorithmToFeedbackTokenMap(PopGeohashRealGraph).toString -> Set(AlgorithmType.Geo), - AlgorithmToFeedbackTokenMap(EngagedFollowerRatio).toString -> Set(AlgorithmType.Geo), - AlgorithmToFeedbackTokenMap(CrowdSearchAccounts).toString -> Set(AlgorithmType.Geo), - AlgorithmToFeedbackTokenMap(OrganicFollowAccounts).toString -> Set(AlgorithmType.Geo), - AlgorithmToFeedbackTokenMap(PopGeohashQualityFollow).toString -> Set(AlgorithmType.Geo), - AlgorithmToFeedbackTokenMap(PPMILocaleFollow).toString -> Set(AlgorithmType.Geo), - // Interest Algorithms: - AlgorithmToFeedbackTokenMap(TttInterest).toString -> Set(AlgorithmType.Interest), - AlgorithmToFeedbackTokenMap(UttInterestRelatedUsers).toString -> Set(AlgorithmType.Interest), - AlgorithmToFeedbackTokenMap(UttSeedAccounts).toString -> Set(AlgorithmType.Interest), - AlgorithmToFeedbackTokenMap(UttProducerExpansion).toString -> Set(AlgorithmType.Interest), - // Hybrid (more than one type) Algorithms: - AlgorithmToFeedbackTokenMap(UttProducerOfflineMbcgV1).toString -> Set( - AlgorithmType.Interest, - AlgorithmType.Geo), - AlgorithmToFeedbackTokenMap(CuratedAccounts).toString -> Set( - AlgorithmType.Interest, - AlgorithmType.Geo), - AlgorithmToFeedbackTokenMap(UserUserGraph).toString -> Set( - AlgorithmType.Social, - AlgorithmType.Activity), - ) - def getAlgorithmTypes(algoId: String): Set[String] = { - AlgorithmIdToType.get(algoId).map(_.map(_.toString)).getOrElse(Set.empty) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/GuiceNamedConstants.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/GuiceNamedConstants.scala deleted file mode 100644 index d3d61fa43..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/GuiceNamedConstants.scala +++ /dev/null @@ -1,43 +0,0 @@ -package com.twitter.follow_recommendations.common.constants - -object GuiceNamedConstants { - final val PRODUCER_SIDE_FEATURE_SWITCHES = "PRODUCER_SIDE_FEATURE_SWITCHES" - final val CLIENT_EVENT_LOGGER = "CLIENT_EVENT_LOGGER" - final val COSINE_FOLLOW_FETCHER = "cosine_follow_fetcher" - final val COSINE_LIST_FETCHER = "cosine_list_fetcher" - final val CURATED_CANDIDATES_FETCHER = "curated_candidates_fetcher" - final val CURATED_COMPETITOR_ACCOUNTS_FETCHER = "curated_competitor_accounts_fetcher" - final val POP_USERS_IN_PLACE_FETCHER = "pop_users_in_place_fetcher" - final val PROFILE_SIDEBAR_BLACKLIST_SCANNER = "profile_sidebar_blacklist_scanner" - final val REQUEST_LOGGER = "REQUEST_LOGGER" - final val FLOW_LOGGER = "FLOW_LOGGER" - final val REAL_TIME_INTERACTIONS_FETCHER = "real_time_interactions_fetcher" - final val SIMS_FETCHER = "sims_fetcher" - final val DBV2_SIMS_FETCHER = "dbv2_sims_fetcher" - - final val TRIANGULAR_LOOPS_FETCHER = "triangular_loops_fetcher" - final val TWO_HOP_RANDOM_WALK_FETCHER = "two_hop_random_walk_fetcher" - final val USER_RECOMMENDABILITY_FETCHER = "user_recommendability_fetcher" - final val USER_STATE_FETCHER = "user_state_fetcher" - final val UTT_ACCOUNT_RECOMMENDATIONS_FETCHER = "utt_account_recomendations_fetcher" - final val UTT_SEED_ACCOUNTS_FETCHER = "utt_seed_accounts_fetcher" - - final val ELECTION_CANDIDATES_FETCHER = "election_candidates_fetcher" - final val POST_NUX_WTF_FEATURES_FETCHER = "post_nux_wtf_features_fetcher" - - final val USER_USER_GRAPH_FETCHER = "user_user_graph_fetcher" - final val DISMISS_STORE_SCANNER = "dismiss_store_scanner" - final val LABELED_NOTIFICATION_FETCHER = "labeled_notification_scanner" - - final val STP_EP_SCORER = "stp_ep_scorer" - final val STP_DBV2_SCORER = "stp_dbv2_scorer" - final val STP_RAB_DBV2_SCORER = "stp_rab_dbv2_scorer" - - final val EXTENDED_NETWORK = "extended_network_candidates" - - // scoring client constants - final val WTF_PROD_DEEPBIRDV2_CLIENT = "wtf_prod_deepbirdv2_client" - - // ann clients - final val RELATABLE_ACCOUNTS_FETCHER = "relatable_accounts_fetcher" -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/ServiceConstants.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/ServiceConstants.scala deleted file mode 100644 index 6aade704c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants/ServiceConstants.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.twitter.follow_recommendations.common.constants - -import com.twitter.conversions.StorageUnitOps._ - -object ServiceConstants { - - /** thrift client response size limits - * these were estimated using monitoring dashboard - * 3MB network usage per second / 25 rps ~ 120KB/req << 1MB - * we give some buffer here in case some requests require more data than others - */ - val StringLengthLimit: Long = - 10.megabyte.inBytes - val ContainerLengthLimit: Long = 1.megabyte.inBytes -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/BUILD deleted file mode 100644 index b4afee590..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/BUILD +++ /dev/null @@ -1,82 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - ":candidate-algorithm-adapter", - ":client-context-adapter", - ":post-nux-algorithm-adapter", - ":pre-fetched-feature-adapter", - ], -) - -target( - name = "common", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/org/slf4j:slf4j-api", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "src/java/com/twitter/ml/api:api-base", - "src/scala/com/twitter/ml/api/util", - "src/scala/com/twitter/onboarding/relevance/util/metadata", - "util/util-slf4j-api/src/main/scala", - ], -) - -scala_library( - name = "candidate-algorithm-adapter", - sources = [ - "CandidateAlgorithmAdapter.scala", - ], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - ":common", - "hermit/hermit-core/src/main/scala/com/twitter/hermit/constants", - ], -) - -scala_library( - name = "client-context-adapter", - sources = [ - "ClientContextAdapter.scala", - ], - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - ":common", - "snowflake/src/main/scala/com/twitter/snowflake/id", - ], -) - -scala_library( - name = "post-nux-algorithm-adapter", - sources = [ - "PostNuxAlgorithmAdapter.scala", - ], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - ":common", - "src/scala/com/twitter/ml/featurestore/catalog/features/customer_journey:post-nux-algorithm-aggregate", - ], -) - -scala_library( - name = "pre-fetched-feature-adapter", - sources = [ - "PreFetchedFeatureAdapter.scala", - ], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - ":common", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/CandidateAlgorithmAdapter.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/CandidateAlgorithmAdapter.scala deleted file mode 100644 index 7a487a95b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/CandidateAlgorithmAdapter.scala +++ /dev/null @@ -1,72 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.adapters - -import com.twitter.follow_recommendations.common.models.UserCandidateSourceDetails -import com.twitter.hermit.constants.AlgorithmFeedbackTokens.AlgorithmToFeedbackTokenMap -import com.twitter.hermit.model.Algorithm -import com.twitter.hermit.model.Algorithm.Algorithm -import com.twitter.hermit.model.Algorithm.UttProducerOfflineMbcgV1 -import com.twitter.hermit.model.Algorithm.UttProducerOnlineMbcgV1 -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.Feature.SparseBinary -import com.twitter.ml.api.Feature.SparseContinuous -import com.twitter.ml.api.FeatureContext -import com.twitter.ml.api.IRecordOneToOneAdapter -import com.twitter.ml.api.util.FDsl._ - -object CandidateAlgorithmAdapter - extends IRecordOneToOneAdapter[Option[UserCandidateSourceDetails]] { - - val CANDIDATE_ALGORITHMS: SparseBinary = new SparseBinary("candidate.source.algorithm_ids") - val CANDIDATE_SOURCE_SCORES: SparseContinuous = - new SparseContinuous("candidate.source.scores") - val CANDIDATE_SOURCE_RANKS: SparseContinuous = - new SparseContinuous("candidate.source.ranks") - - override val getFeatureContext: FeatureContext = new FeatureContext( - CANDIDATE_ALGORITHMS, - CANDIDATE_SOURCE_SCORES, - CANDIDATE_SOURCE_RANKS - ) - - /** list of candidate source remaps to avoid creating different features for experimental sources. - * the LHS should contain the experimental source, and the RHS should contain the prod source. - */ - def remapCandidateSource(a: Algorithm): Algorithm = a match { - case UttProducerOnlineMbcgV1 => UttProducerOfflineMbcgV1 - case _ => a - } - - // add the list of algorithm feedback tokens (integers) as a sparse binary feature - override def adaptToDataRecord( - userCandidateSourceDetailsOpt: Option[UserCandidateSourceDetails] - ): DataRecord = { - val dr = new DataRecord() - userCandidateSourceDetailsOpt.foreach { userCandidateSourceDetails => - val scoreMap = for { - (source, scoreOpt) <- userCandidateSourceDetails.candidateSourceScores - score <- scoreOpt - algo <- Algorithm.withNameOpt(source.name) - algoId <- AlgorithmToFeedbackTokenMap.get(remapCandidateSource(algo)) - } yield algoId.toString -> score - val rankMap = for { - (source, rank) <- userCandidateSourceDetails.candidateSourceRanks - algo <- Algorithm.withNameOpt(source.name) - algoId <- AlgorithmToFeedbackTokenMap.get(remapCandidateSource(algo)) - } yield algoId.toString -> rank.toDouble - - val algoIds = scoreMap.keys.toSet ++ rankMap.keys.toSet - - // hydrate if not empty - if (rankMap.nonEmpty) { - dr.setFeatureValue(CANDIDATE_SOURCE_RANKS, rankMap) - } - if (scoreMap.nonEmpty) { - dr.setFeatureValue(CANDIDATE_SOURCE_SCORES, scoreMap) - } - if (algoIds.nonEmpty) { - dr.setFeatureValue(CANDIDATE_ALGORITHMS, algoIds) - } - } - dr - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/ClientContextAdapter.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/ClientContextAdapter.scala deleted file mode 100644 index 9aa4bdb0d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/ClientContextAdapter.scala +++ /dev/null @@ -1,79 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.adapters - -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.ml.api.Feature.Binary -import com.twitter.ml.api.Feature.Continuous -import com.twitter.ml.api.Feature.Discrete -import com.twitter.ml.api.Feature.Text -import com.twitter.ml.api.util.FDsl._ -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.ml.api.IRecordOneToOneAdapter -import com.twitter.onboarding.relevance.util.metadata.LanguageUtil -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext -import com.twitter.snowflake.id.SnowflakeId - -object ClientContextAdapter extends IRecordOneToOneAdapter[(ClientContext, DisplayLocation)] { - - // we name features with `user.account` for relatively static user-related features - val USER_COUNTRY: Text = new Text("user.account.country") - val USER_LANGUAGE: Text = new Text("user.account.language") - // we name features with `user.context` for more dynamic user-related features - val USER_LANGUAGE_PREFIX: Text = new Text("user.context.language_prefix") - val USER_CLIENT: Discrete = new Discrete("user.context.client") - val USER_AGE: Continuous = new Continuous("user.context.age") - val USER_IS_RECENT: Binary = new Binary("user.is.recent") - // we name features with `meta` for meta info about the WTF recommendation request - val META_DISPLAY_LOCATION: Text = new Text("meta.display_location") - val META_POSITION: Discrete = new Discrete("meta.position") - // This indicates whether a data point is from a random serving policy - val META_IS_RANDOM: Binary = new Binary("prediction.engine.is_random") - - val RECENT_WIN_IN_DAYS: Int = 30 - val GOAL_META_POSITION: Long = 1L - val GOAL_META_IS_RANDOM: Boolean = true - - override val getFeatureContext: FeatureContext = new FeatureContext( - USER_COUNTRY, - USER_LANGUAGE, - USER_AGE, - USER_LANGUAGE_PREFIX, - USER_CLIENT, - USER_IS_RECENT, - META_DISPLAY_LOCATION, - META_POSITION, - META_IS_RANDOM - ) - - /** - * we only want to set the relevant fields iff they exist to eliminate redundant information - * we do some simple normalization on the language code - * we set META_POSITION to 1 always - * we set META_IS_RANDOM to true always to simulate a random serving distribution - * @param record ClientContext and DisplayLocation from the request - */ - override def adaptToDataRecord(target: (ClientContext, DisplayLocation)): DataRecord = { - val dr = new DataRecord() - val cc = target._1 - val dl = target._2 - cc.countryCode.foreach(countryCode => dr.setFeatureValue(USER_COUNTRY, countryCode)) - cc.languageCode.foreach(rawLanguageCode => { - val userLanguage = LanguageUtil.simplifyLanguage(rawLanguageCode) - val userLanguagePrefix = userLanguage.take(2) - dr.setFeatureValue(USER_LANGUAGE, userLanguage) - dr.setFeatureValue(USER_LANGUAGE_PREFIX, userLanguagePrefix) - }) - cc.appId.foreach(appId => dr.setFeatureValue(USER_CLIENT, appId)) - cc.userId.foreach(id => - SnowflakeId.timeFromIdOpt(id).map { signupTime => - val userAge = signupTime.untilNow.inMillis.toDouble - dr.setFeatureValue(USER_AGE, userAge) - dr.setFeatureValue(USER_IS_RECENT, signupTime.untilNow.inDays <= RECENT_WIN_IN_DAYS) - signupTime.untilNow.inDays - }) - dr.setFeatureValue(META_DISPLAY_LOCATION, dl.toFsName) - dr.setFeatureValue(META_POSITION, GOAL_META_POSITION) - dr.setFeatureValue(META_IS_RANDOM, GOAL_META_IS_RANDOM) - dr - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/PostNuxAlgorithmAdapter.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/PostNuxAlgorithmAdapter.scala deleted file mode 100644 index e8fe745a0..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/PostNuxAlgorithmAdapter.scala +++ /dev/null @@ -1,151 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.adapters - -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.Feature -import com.twitter.ml.api.Feature.Continuous -import com.twitter.ml.api.FeatureContext -import com.twitter.ml.api.IRecordOneToOneAdapter -import com.twitter.ml.api.util.FDsl._ -import com.twitter.ml.featurestore.catalog.features.customer_journey.PostNuxAlgorithmFeatures -import com.twitter.ml.featurestore.catalog.features.customer_journey.PostNuxAlgorithmIdAggregateFeatureGroup -import com.twitter.ml.featurestore.catalog.features.customer_journey.PostNuxAlgorithmTypeAggregateFeatureGroup -import scala.collection.JavaConverters._ - -object PostNuxAlgorithmIdAdapter extends PostNuxAlgorithmAdapter { - override val PostNuxAlgorithmFeatureGroup: PostNuxAlgorithmFeatures = - PostNuxAlgorithmIdAggregateFeatureGroup - - // To keep the length of feature names reasonable, we remove the prefix added by FeatureStore. - override val FeatureStorePrefix: String = - "wtf_algorithm_id.customer_journey.post_nux_algorithm_id_aggregate_feature_group." -} - -object PostNuxAlgorithmTypeAdapter extends PostNuxAlgorithmAdapter { - override val PostNuxAlgorithmFeatureGroup: PostNuxAlgorithmFeatures = - PostNuxAlgorithmTypeAggregateFeatureGroup - - // To keep the length of feature names reasonable, we remove the prefix added by FeatureStore. - override val FeatureStorePrefix: String = - "wtf_algorithm_type.customer_journey.post_nux_algorithm_type_aggregate_feature_group." -} - -trait PostNuxAlgorithmAdapter extends IRecordOneToOneAdapter[DataRecord] { - - val PostNuxAlgorithmFeatureGroup: PostNuxAlgorithmFeatures - - // The string that is attached to the feature name when it is fetched from feature store. - val FeatureStorePrefix: String - - /** - * - * This stores transformed aggregate features for PostNux algorithm aggregate features. The - * transformation here is log-ratio, where ratio is the raw value divided by # of impressions. - */ - case class TransformedAlgorithmFeatures( - ratioLog: Continuous) { - def getFeatures: Seq[Continuous] = Seq(ratioLog) - } - - private def applyFeatureStorePrefix(feature: Continuous) = new Continuous( - s"$FeatureStorePrefix${feature.getFeatureName}") - - // The list of input features WITH the prefix assigned to them by FeatureStore. - lazy val allInputFeatures: Seq[Seq[Continuous]] = Seq( - PostNuxAlgorithmFeatureGroup.Aggregate7DayFeatures.map(applyFeatureStorePrefix), - PostNuxAlgorithmFeatureGroup.Aggregate30DayFeatures.map(applyFeatureStorePrefix) - ) - - // This is a list of the features WITHOUT the prefix assigned to them by FeatureStore. - lazy val outputBaseFeatureNames: Seq[Seq[Continuous]] = Seq( - PostNuxAlgorithmFeatureGroup.Aggregate7DayFeatures, - PostNuxAlgorithmFeatureGroup.Aggregate30DayFeatures - ) - - // We use backend impression to calculate ratio values. - lazy val ratioDenominators: Seq[Continuous] = Seq( - applyFeatureStorePrefix(PostNuxAlgorithmFeatureGroup.BackendImpressions7Days), - applyFeatureStorePrefix(PostNuxAlgorithmFeatureGroup.BackendImpressions30Days) - ) - - /** - * A mapping from an original feature's ID to the corresponding set of transformed features. - * This is used to compute the transformed features for each of the original ones. - */ - private lazy val TransformedFeaturesMap: Map[Continuous, TransformedAlgorithmFeatures] = - outputBaseFeatureNames.flatten.map { feature => - ( - // The input feature would have the FeatureStore prefix attached to it. - new Continuous(s"$FeatureStorePrefix${feature.getFeatureName}"), - // We don't keep the FeatureStore prefix to keep the length of feature names reasonable. - TransformedAlgorithmFeatures( - new Continuous(s"${feature.getFeatureName}-ratio-log") - )) - }.toMap - - /** - * Given a denominator, number of impressions, this function returns another function that adds - * transformed features (log1p and ratio) of an input feature to a DataRecord. - */ - private def addTransformedFeaturesToDataRecordFunc( - originalDr: DataRecord, - numImpressions: Double, - ): (DataRecord, Continuous) => DataRecord = { (record: DataRecord, feature: Continuous) => - { - Option(originalDr.getFeatureValue(feature)) foreach { featureValue => - TransformedFeaturesMap.get(feature).foreach { transformedFeatures => - record.setFeatureValue( - transformedFeatures.ratioLog, - // We don't use log1p here since the values are ratios and adding 1 to the _ratio_ would - // lead to logarithm of values between 1 and 2, essentially making all values the same. - math.log((featureValue + 1) / numImpressions) - ) - } - } - record - } - } - - /** - * @param record: The input record whose PostNuxAlgorithm aggregates are to be transformed. - * @return the input [[DataRecord]] with transformed aggregates added. - */ - override def adaptToDataRecord(record: DataRecord): DataRecord = { - if (record.continuousFeatures == null) { - // There are no base features available, and hence no transformations. - record - } else { - - /** - * The `foldLeft` below goes through pairs of (1) Feature groups, such as those calculated over - * 7 days or 30 days, and (2) the number of impressions for each of these groups, which is the - * denominator when ratio is calculated. - */ - ratioDenominators - .zip(allInputFeatures).foldLeft( /* initial empty DataRecord */ record)( - ( - /* DataRecord with transformed features up to here */ transformedRecord, - /* A tuple with the denominator (#impressions) and features to be transformed */ numImpressionsAndFeatures - ) => { - val (numImpressionsFeature, features) = numImpressionsAndFeatures - Option(record.getFeatureValue(numImpressionsFeature)) match { - case Some(numImpressions) if numImpressions > 0.0 => - /** - * With the number of impressions fixed, we generate a function that adds log-ratio - * for each feature in the current [[DataRecord]]. The `foldLeft` goes through all - * such features and applies that function while updating the kept DataRecord. - */ - features.foldLeft(transformedRecord)( - addTransformedFeaturesToDataRecordFunc(record, numImpressions)) - case _ => - transformedRecord - } - }) - } - } - - def getFeatures: Seq[Feature[_]] = TransformedFeaturesMap.values.flatMap(_.getFeatures).toSeq - - override def getFeatureContext: FeatureContext = - new FeatureContext() - .addFeatures(this.getFeatures.asJava) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/PreFetchedFeatureAdapter.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/PreFetchedFeatureAdapter.scala deleted file mode 100644 index f24ed1efe..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters/PreFetchedFeatureAdapter.scala +++ /dev/null @@ -1,91 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.adapters - -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.ml.api.Feature.Continuous -import com.twitter.ml.api.util.FDsl._ -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.ml.api.IRecordOneToOneAdapter -import com.twitter.util.Time - -/** - * This adapter mimics UserRecentWTFImpressionsAndFollowsAdapter (for user) and - * RecentWTFImpressionsFeatureAdapter (for candidate) for extracting recent impression - * and follow features. This adapter extracts user, candidate, and pair-wise features. - */ -object PreFetchedFeatureAdapter - extends IRecordOneToOneAdapter[ - (HasPreFetchedFeature, CandidateUser) - ] { - - // impression features - val USER_NUM_RECENT_IMPRESSIONS: Continuous = new Continuous( - "user.prefetch.num_recent_impressions" - ) - val USER_LAST_IMPRESSION_DURATION: Continuous = new Continuous( - "user.prefetch.last_impression_duration" - ) - val CANDIDATE_NUM_RECENT_IMPRESSIONS: Continuous = new Continuous( - "user-candidate.prefetch.num_recent_impressions" - ) - val CANDIDATE_LAST_IMPRESSION_DURATION: Continuous = new Continuous( - "user-candidate.prefetch.last_impression_duration" - ) - // follow features - val USER_NUM_RECENT_FOLLOWERS: Continuous = new Continuous( - "user.prefetch.num_recent_followers" - ) - val USER_NUM_RECENT_FOLLOWED_BY: Continuous = new Continuous( - "user.prefetch.num_recent_followed_by" - ) - val USER_NUM_RECENT_MUTUAL_FOLLOWS: Continuous = new Continuous( - "user.prefetch.num_recent_mutual_follows" - ) - // impression + follow features - val USER_NUM_RECENT_FOLLOWED_IMPRESSIONS: Continuous = new Continuous( - "user.prefetch.num_recent_followed_impression" - ) - val USER_LAST_FOLLOWED_IMPRESSION_DURATION: Continuous = new Continuous( - "user.prefetch.last_followed_impression_duration" - ) - - override def adaptToDataRecord( - record: (HasPreFetchedFeature, CandidateUser) - ): DataRecord = { - val (target, candidate) = record - val dr = new DataRecord() - val t = Time.now - // set impression features for user, optionally for candidate - dr.setFeatureValue(USER_NUM_RECENT_IMPRESSIONS, target.numWtfImpressions.toDouble) - dr.setFeatureValue( - USER_LAST_IMPRESSION_DURATION, - (t - target.latestImpressionTime).inMillis.toDouble) - target.getCandidateImpressionCounts(candidate.id).foreach { counts => - dr.setFeatureValue(CANDIDATE_NUM_RECENT_IMPRESSIONS, counts.toDouble) - } - target.getCandidateLatestTime(candidate.id).foreach { latestTime: Time => - dr.setFeatureValue(CANDIDATE_LAST_IMPRESSION_DURATION, (t - latestTime).inMillis.toDouble) - } - // set recent follow features for user - dr.setFeatureValue(USER_NUM_RECENT_FOLLOWERS, target.numRecentFollowedUserIds.toDouble) - dr.setFeatureValue(USER_NUM_RECENT_FOLLOWED_BY, target.numRecentFollowedByUserIds.toDouble) - dr.setFeatureValue(USER_NUM_RECENT_MUTUAL_FOLLOWS, target.numRecentMutualFollows.toDouble) - dr.setFeatureValue(USER_NUM_RECENT_FOLLOWED_IMPRESSIONS, target.numFollowedImpressions.toDouble) - dr.setFeatureValue( - USER_LAST_FOLLOWED_IMPRESSION_DURATION, - target.lastFollowedImpressionDurationMs.getOrElse(Long.MaxValue).toDouble) - dr - } - override def getFeatureContext: FeatureContext = new FeatureContext( - USER_NUM_RECENT_IMPRESSIONS, - USER_LAST_IMPRESSION_DURATION, - CANDIDATE_NUM_RECENT_IMPRESSIONS, - CANDIDATE_LAST_IMPRESSION_DURATION, - USER_NUM_RECENT_FOLLOWERS, - USER_NUM_RECENT_FOLLOWED_BY, - USER_NUM_RECENT_MUTUAL_FOLLOWS, - USER_NUM_RECENT_FOLLOWED_IMPRESSIONS, - USER_LAST_FOLLOWED_IMPRESSION_DURATION, - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/BUILD deleted file mode 100644 index 93ddb1191..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/BUILD +++ /dev/null @@ -1,18 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "src/java/com/twitter/ml/api:api-base", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/FeatureSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/FeatureSource.scala deleted file mode 100644 index 9d43f0c13..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/FeatureSource.scala +++ /dev/null @@ -1,23 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.common - -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -trait FeatureSource { - def id: FeatureSourceId - def featureContext: FeatureContext - def hydrateFeatures( - target: HasClientContext - with HasPreFetchedFeature - with HasParams - with HasSimilarToContext - with HasDisplayLocation, - candidates: Seq[CandidateUser] - ): Stitch[Map[CandidateUser, DataRecord]] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/FeatureSourceId.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/FeatureSourceId.scala deleted file mode 100644 index 66b883120..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/FeatureSourceId.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.common - -sealed trait FeatureSourceId - -object FeatureSourceId { - object CandidateAlgorithmSourceId extends FeatureSourceId - object ClientContextSourceId extends FeatureSourceId - object FeatureStoreSourceId extends FeatureSourceId - object FeatureStoreTimelinesAuthorSourceId extends FeatureSourceId - object FeatureStoreGizmoduckSourceId extends FeatureSourceId - object FeatureStoreUserMetricCountsSourceId extends FeatureSourceId - object FeatureStoreNotificationSourceId extends FeatureSourceId - - object FeatureStorePrecomputedNotificationSourceId extends FeatureSourceId - object FeatureStorePostNuxAlgorithmSourceId extends FeatureSourceId - @deprecated object StratoFeatureHydrationSourceId extends FeatureSourceId - object PreFetchedFeatureSourceId extends FeatureSourceId - object UserScoringFeatureSourceId extends FeatureSourceId -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/HasPreFetchedFeature.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/HasPreFetchedFeature.scala deleted file mode 100644 index 8f4bae887..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common/HasPreFetchedFeature.scala +++ /dev/null @@ -1,25 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.common - -import com.twitter.follow_recommendations.common.models.HasMutualFollowedUserIds -import com.twitter.follow_recommendations.common.models.HasWtfImpressions -import com.twitter.follow_recommendations.common.models.WtfImpression -import com.twitter.util.Time - -trait HasPreFetchedFeature extends HasMutualFollowedUserIds with HasWtfImpressions { - - lazy val followedImpressions: Seq[WtfImpression] = { - for { - wtfImprList <- wtfImpressions.toSeq - wtfImpr <- wtfImprList - if recentFollowedUserIds.exists(_.contains(wtfImpr.candidateId)) - } yield wtfImpr - } - - lazy val numFollowedImpressions: Int = followedImpressions.size - - lazy val lastFollowedImpressionDurationMs: Option[Long] = { - if (followedImpressions.nonEmpty) { - Some((Time.now - followedImpressions.map(_.latestTime).max).inMillis) - } else None - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/BUILD deleted file mode 100644 index c0538240f..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/BUILD +++ /dev/null @@ -1,59 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "escherbird/src/scala/com/twitter/escherbird/util/stitchcache", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/adapters", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "hermit/hermit-core/src/main/scala/com/twitter/hermit/constants", - "src/java/com/twitter/ml/api:api-base", - "src/scala/com/twitter/ml/featurestore/catalog/datasets/core:socialgraph", - "src/scala/com/twitter/ml/featurestore/catalog/datasets/core:usersource", - "src/scala/com/twitter/ml/featurestore/catalog/datasets/onboarding:mc-user-counting", - "src/scala/com/twitter/ml/featurestore/catalog/datasets/onboarding:user-wtf-algorithm-aggregate", - "src/scala/com/twitter/ml/featurestore/catalog/datasets/onboarding:wtf-impression", - "src/scala/com/twitter/ml/featurestore/catalog/datasets/onboarding:wtf-post-nux", - "src/scala/com/twitter/ml/featurestore/catalog/datasets/onboarding:wtf-user-algorithm-aggregate", - "src/scala/com/twitter/ml/featurestore/catalog/datasets/timelines:timelines-author-features", - "src/scala/com/twitter/ml/featurestore/catalog/entities/core", - "src/scala/com/twitter/ml/featurestore/catalog/entities/onboarding", - "src/scala/com/twitter/ml/featurestore/catalog/features/core:socialgraph", - "src/scala/com/twitter/ml/featurestore/catalog/features/core:user", - "src/scala/com/twitter/ml/featurestore/catalog/features/interests_discovery:user-topic-relationships", - "src/scala/com/twitter/ml/featurestore/catalog/features/magicrecs:non-mr-notif-summmaries", - "src/scala/com/twitter/ml/featurestore/catalog/features/magicrecs:non-mr-notif-summmary-aggregates", - "src/scala/com/twitter/ml/featurestore/catalog/features/magicrecs:nonmr-ntab-summaries", - "src/scala/com/twitter/ml/featurestore/catalog/features/onboarding:mc-user-counting", - "src/scala/com/twitter/ml/featurestore/catalog/features/onboarding:post-nux-offline", - "src/scala/com/twitter/ml/featurestore/catalog/features/onboarding:post-nux-offline-edge", - "src/scala/com/twitter/ml/featurestore/catalog/features/onboarding:ratio", - "src/scala/com/twitter/ml/featurestore/catalog/features/onboarding:simcluster-user-interested-in-candidate-known-for", - "src/scala/com/twitter/ml/featurestore/catalog/features/onboarding:user-wtf-algorithm-aggregate", - "src/scala/com/twitter/ml/featurestore/catalog/features/onboarding:wtf-impression", - "src/scala/com/twitter/ml/featurestore/catalog/features/onboarding:wtf-user-algorithm-aggregate", - "src/scala/com/twitter/ml/featurestore/catalog/features/rux:user-resurrection", - "src/scala/com/twitter/ml/featurestore/catalog/features/timelines:aggregate", - "src/scala/com/twitter/ml/featurestore/lib", - "src/scala/com/twitter/ml/featurestore/lib/dynamic", - "src/scala/com/twitter/ml/featurestore/lib/embedding", - "src/scala/com/twitter/ml/featurestore/lib/feature", - "src/scala/com/twitter/ml/featurestore/lib/online", - "src/scala/com/twitter/ml/featurestore/lib/params", - "src/scala/com/twitter/onboarding/relevance/adapters/features/featurestore", - "strato/config/columns/ml/featureStore:featureStore-strato-client", - "strato/config/columns/ml/featureStore/onboarding:onboarding-strato-client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/CandidateAlgorithmSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/CandidateAlgorithmSource.scala deleted file mode 100644 index 0838fb98d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/CandidateAlgorithmSource.scala +++ /dev/null @@ -1,73 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.google.inject.Inject -import com.google.inject.Provides -import com.google.inject.Singleton -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.feature_hydration.adapters.CandidateAlgorithmAdapter -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSource -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSourceId -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -/** - * This source only takes features from the candidate's source, - * which is all the information we have about the candidate pre-feature-hydration - */ - -@Provides -@Singleton -class CandidateAlgorithmSource @Inject() (stats: StatsReceiver) extends FeatureSource { - - override val id: FeatureSourceId = FeatureSourceId.CandidateAlgorithmSourceId - - override val featureContext: FeatureContext = CandidateAlgorithmAdapter.getFeatureContext - - override def hydrateFeatures( - t: HasClientContext - with HasPreFetchedFeature - with HasParams - with HasSimilarToContext - with HasDisplayLocation, // we don't use the target here - candidates: Seq[CandidateUser] - ): Stitch[Map[CandidateUser, DataRecord]] = { - val featureHydrationStats = stats.scope("candidate_alg_source") - val hasSourceDetailsStat = featureHydrationStats.counter("has_source_details") - val noSourceDetailsStat = featureHydrationStats.counter("no_source_details") - val noSourceRankStat = featureHydrationStats.counter("no_source_rank") - val hasSourceRankStat = featureHydrationStats.counter("has_source_rank") - val noSourceScoreStat = featureHydrationStats.counter("no_source_score") - val hasSourceScoreStat = featureHydrationStats.counter("has_source_score") - - val candidatesToAlgoMap = for { - candidate <- candidates - } yield { - if (candidate.userCandidateSourceDetails.nonEmpty) { - hasSourceDetailsStat.incr() - candidate.userCandidateSourceDetails.foreach { details => - if (details.candidateSourceRanks.isEmpty) { - noSourceRankStat.incr() - } else { - hasSourceRankStat.incr() - } - if (details.candidateSourceScores.isEmpty) { - noSourceScoreStat.incr() - } else { - hasSourceScoreStat.incr() - } - } - } else { - noSourceDetailsStat.incr() - } - candidate -> CandidateAlgorithmAdapter.adaptToDataRecord(candidate.userCandidateSourceDetails) - } - Stitch.value(candidatesToAlgoMap.toMap) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/ClientContextSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/ClientContextSource.scala deleted file mode 100644 index 718502d44..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/ClientContextSource.scala +++ /dev/null @@ -1,43 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.google.inject.Provides -import com.google.inject.Singleton -import com.twitter.follow_recommendations.common.feature_hydration.adapters.ClientContextAdapter -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSource -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSourceId -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -/** - * This source only takes features from the request (e.g. client context, WTF display location) - * No external calls are made. - */ -@Provides -@Singleton -class ClientContextSource() extends FeatureSource { - - override val id: FeatureSourceId = FeatureSourceId.ClientContextSourceId - - override val featureContext: FeatureContext = ClientContextAdapter.getFeatureContext - - override def hydrateFeatures( - t: HasClientContext - with HasPreFetchedFeature - with HasParams - with HasSimilarToContext - with HasDisplayLocation, - candidates: Seq[CandidateUser] - ): Stitch[Map[CandidateUser, DataRecord]] = { - Stitch.value( - candidates - .map(_ -> ((t.clientContext, t.displayLocation))).toMap.mapValues( - ClientContextAdapter.adaptToDataRecord)) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureHydrationSourcesFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureHydrationSourcesFSConfig.scala deleted file mode 100644 index f78ec17cc..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureHydrationSourcesFSConfig.scala +++ /dev/null @@ -1,42 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.timelines.configapi.Param -import com.twitter.util.Duration -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class FeatureHydrationSourcesFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = Seq( - FeatureStoreSourceParams.EnableAlgorithmAggregateFeatures, - FeatureStoreSourceParams.EnableAuthorTopicAggregateFeatures, - FeatureStoreSourceParams.EnableCandidateClientFeatures, - FeatureStoreSourceParams.EnableCandidatePrecomputedNotificationFeatures, - FeatureStoreSourceParams.EnableCandidateUserAuthorRealTimeAggregateFeatures, - FeatureStoreSourceParams.EnableCandidateUserFeatures, - FeatureStoreSourceParams.EnableCandidateUserResurrectionFeatures, - FeatureStoreSourceParams.EnableCandidateUserTimelinesAuthorAggregateFeatures, - FeatureStoreSourceParams.EnableSeparateClientForTimelinesAuthors, - FeatureStoreSourceParams.EnableSeparateClientForGizmoduck, - FeatureStoreSourceParams.EnableSeparateClientForMetricCenterUserCounting, - FeatureStoreSourceParams.EnableSeparateClientForNotifications, - FeatureStoreSourceParams.EnableSimilarToUserFeatures, - FeatureStoreSourceParams.EnableTargetUserFeatures, - FeatureStoreSourceParams.EnableTargetUserResurrectionFeatures, - FeatureStoreSourceParams.EnableTargetUserWtfImpressionFeatures, - FeatureStoreSourceParams.EnableTopicAggregateFeatures, - FeatureStoreSourceParams.EnableUserCandidateEdgeFeatures, - FeatureStoreSourceParams.EnableUserCandidateWtfImpressionCandidateFeatures, - FeatureStoreSourceParams.EnableUserClientFeatures, - FeatureStoreSourceParams.EnableUserTopicFeatures, - FeatureStoreSourceParams.EnableUserWtfAlgEdgeFeatures, - ) - - override val durationFSParams: Seq[FSBoundedParam[Duration] with HasDurationConversion] = Seq( - FeatureStoreSourceParams.GlobalFetchTimeout - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureHydrationSourcesFeatureSwitchKeys.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureHydrationSourcesFeatureSwitchKeys.scala deleted file mode 100644 index fb2232927..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureHydrationSourcesFeatureSwitchKeys.scala +++ /dev/null @@ -1,42 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -object FeatureHydrationSourcesFeatureSwitchKeys { - val EnableAlgorithmAggregateFeatures = "feature_store_source_enable_algorithm_aggregate_features" - val EnableAuthorTopicAggregateFeatures = - "feature_store_source_enable_author_topic_aggregate_features" - val EnableCandidateClientFeatures = "feature_store_source_enable_candidate_client_features" - val EnableCandidateNotificationFeatures = - "feature_store_source_enable_candidate_notification_features" - val EnableCandidatePrecomputedNotificationFeatures = - "feature_store_source_enable_candidate_precomputed_notification_features" - val EnableCandidateUserFeatures = "feature_store_source_enable_candidate_user_features" - val EnableCandidateUserAuthorRealTimeAggregateFeatures = - "feature_store_source_enable_candidate_user_author_rta_features" - val EnableCandidateUserResurrectionFeatures = - "feature_store_source_enable_candidate_user_resurrection_features" - val EnableCandidateUserTimelinesAuthorAggregateFeatures = - "feature_store_source_enable_candidate_user_timelines_author_aggregate_features" - val EnableSimilarToUserFeatures = "feature_store_source_enable_similar_to_user_features" - val EnableTargetUserFeatures = "feature_store_source_enable_target_user_features" - val EnableTargetUserUserAuthorUserStateRealTimeAggregatesFeature = - "feature_store_source_enable_target_user_user_author_user_state_rta_features" - val EnableTargetUserResurrectionFeatures = - "feature_store_source_enable_target_user_resurrection_features" - val EnableTargetUserWtfImpressionFeatures = - "feature_store_source_enable_target_user_wtf_impression_features" - val EnableTopicAggregateFeatures = "feature_store_source_enable_topic_aggregate_features" - val EnableUserCandidateEdgeFeatures = "feature_store_source_enable_user_candidate_edge_features" - val EnableUserCandidateWtfImpressionCandidateFeatures = - "feature_store_source_enable_user_candidate_wtf_impression_features" - val EnableUserClientFeatures = "feature_store_source_enable_user_client_features" - val EnableUserNotificationFeatures = "feature_store_source_enable_user_notification_features" - val EnableUserTopicFeatures = "feature_store_source_enable_user_topic_features" - val EnableUserWtfAlgEdgeFeatures = "feature_store_source_enable_user_wtf_alg_edge_features" - val FeatureHydrationTimeout = "feature_store_source_hydration_timeout_in_millis" - val UseSeparateClientForTimelinesAuthor = - "feature_store_source_separate_client_for_timelines_author_data" - val UseSeparateClientMetricCenterUserCounting = - "feature_store_source_separate_client_for_mc_user_counting_data" - val UseSeparateClientForNotifications = "feature_store_source_separate_client_for_notifications" - val UseSeparateClientForGizmoduck = "feature_store_source_separate_client_for_gizmoduck" -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreFeatures.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreFeatures.scala deleted file mode 100644 index 6c849f7a7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreFeatures.scala +++ /dev/null @@ -1,342 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.ml.featurestore.catalog.entities.core.{Author => AuthorEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{AuthorTopic => AuthorTopicEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{CandidateUser => CandidateUserEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{Topic => TopicEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{User => UserEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{UserCandidate => UserCandidateEntity} -import com.twitter.ml.featurestore.catalog.entities.onboarding.UserWtfAlgorithmEntity -import com.twitter.ml.featurestore.catalog.entities.onboarding.{ - WtfAlgorithm => WtfAlgorithmIdEntity -} -import com.twitter.ml.featurestore.catalog.entities.onboarding.{ - WtfAlgorithmType => WtfAlgorithmTypeEntity -} -import com.twitter.ml.featurestore.catalog.features.core.UserClients.FullPrimaryClientVersion -import com.twitter.ml.featurestore.catalog.features.core.UserClients.NumClients -import com.twitter.ml.featurestore.catalog.features.core.UserClients.PrimaryClient -import com.twitter.ml.featurestore.catalog.features.core.UserClients.PrimaryClientVersion -import com.twitter.ml.featurestore.catalog.features.core.UserClients.PrimaryDeviceManufacturer -import com.twitter.ml.featurestore.catalog.features.core.UserClients.PrimaryMobileSdkVersion -import com.twitter.ml.featurestore.catalog.features.core.UserClients.SecondaryClient -import com.twitter.ml.featurestore.catalog.features.core.UserCounts.Favorites -import com.twitter.ml.featurestore.catalog.features.core.UserCounts.Followers -import com.twitter.ml.featurestore.catalog.features.core.UserCounts.Following -import com.twitter.ml.featurestore.catalog.features.core.UserCounts.Tweets -import com.twitter.ml.featurestore.catalog.features.customer_journey.PostNuxAlgorithmIdAggregateFeatureGroup -import com.twitter.ml.featurestore.catalog.features.customer_journey.PostNuxAlgorithmTypeAggregateFeatureGroup -import com.twitter.ml.featurestore.catalog.features.customer_journey.{Utils => FeatureGroupUtils} -import com.twitter.ml.featurestore.catalog.features.interests_discovery.UserTopicRelationships.FollowedTopics -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumFavorites -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumFavoritesReceived -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumFollowBacks -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumFollows -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumFollowsReceived -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumLoginDays -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumLoginTweetImpressions -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumMuteBacks -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumMuted -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumOriginalTweets -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumQualityFollowReceived -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumQuoteRetweets -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumQuoteRetweetsReceived -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumReplies -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumRepliesReceived -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumRetweets -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumRetweetsReceived -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumSpamBlocked -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumSpamBlockedBacks -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumTweetImpressions -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumTweets -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumUnfollowBacks -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumUnfollows -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumUserActiveMinutes -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumWasMutualFollowed -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumWasMutualUnfollowed -import com.twitter.ml.featurestore.catalog.features.onboarding.MetricCenterUserCounts.NumWasUnfollowed -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOffline.Country -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOffline.FollowersOverFollowingRatio -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOffline.Language -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOffline.MutualFollowsOverFollowersRatio -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOffline.MutualFollowsOverFollowingRatio -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOffline.NumFollowers -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOffline.NumFollowings -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOffline.NumMutualFollows -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOffline.TweepCred -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOffline.UserState -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOfflineEdge.HaveSameCountry -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOfflineEdge.HaveSameLanguage -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOfflineEdge.HaveSameUserState -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOfflineEdge.NumFollowersGap -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOfflineEdge.NumFollowingsGap -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOfflineEdge.NumMutualFollowsGap -import com.twitter.ml.featurestore.catalog.features.onboarding.PostNuxOfflineEdge.TweepCredGap -import com.twitter.ml.featurestore.catalog.features.onboarding.Ratio.FollowersFollowings -import com.twitter.ml.featurestore.catalog.features.onboarding.Ratio.MutualFollowsFollowing -import com.twitter.ml.featurestore.catalog.features.onboarding.SimclusterUserInterestedInCandidateKnownFor.HasIntersection -import com.twitter.ml.featurestore.catalog.features.onboarding.SimclusterUserInterestedInCandidateKnownFor.IntersectionCandidateKnownForScore -import com.twitter.ml.featurestore.catalog.features.onboarding.SimclusterUserInterestedInCandidateKnownFor.IntersectionClusterIds -import com.twitter.ml.featurestore.catalog.features.onboarding.SimclusterUserInterestedInCandidateKnownFor.IntersectionUserFavCandidateKnownForScore -import com.twitter.ml.featurestore.catalog.features.onboarding.SimclusterUserInterestedInCandidateKnownFor.IntersectionUserFavScore -import com.twitter.ml.featurestore.catalog.features.onboarding.SimclusterUserInterestedInCandidateKnownFor.IntersectionUserFollowCandidateKnownForScore -import com.twitter.ml.featurestore.catalog.features.onboarding.SimclusterUserInterestedInCandidateKnownFor.IntersectionUserFollowScore -import com.twitter.ml.featurestore.catalog.features.onboarding.UserWtfAlgorithmAggregate -import com.twitter.ml.featurestore.catalog.features.onboarding.WhoToFollowImpression.HomeTimelineWtfCandidateCounts -import com.twitter.ml.featurestore.catalog.features.onboarding.WhoToFollowImpression.HomeTimelineWtfCandidateImpressionCounts -import com.twitter.ml.featurestore.catalog.features.onboarding.WhoToFollowImpression.HomeTimelineWtfCandidateImpressionLatestTimestamp -import com.twitter.ml.featurestore.catalog.features.onboarding.WhoToFollowImpression.HomeTimelineWtfLatestTimestamp -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.FollowRate -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.Follows -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.FollowsTweetFavRate -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.FollowsTweetReplies -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.FollowsTweetReplyRate -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.FollowsTweetRetweetRate -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.FollowsTweetRetweets -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.FollowsWithTweetFavs -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.FollowsWithTweetImpressions -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.HasAnyEngagements -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.HasForwardEngagements -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.HasReverseEngagements -import com.twitter.ml.featurestore.catalog.features.onboarding.WtfUserAlgorithmAggregate.Impressions -import com.twitter.ml.featurestore.catalog.features.rux.UserResurrection.DaysSinceRecentResurrection -import com.twitter.ml.featurestore.catalog.features.timelines.AuthorTopicAggregates -import com.twitter.ml.featurestore.catalog.features.timelines.EngagementsReceivedByAuthorRealTimeAggregates -import com.twitter.ml.featurestore.catalog.features.timelines.NegativeEngagementsReceivedByAuthorRealTimeAggregates -import com.twitter.ml.featurestore.catalog.features.timelines.OriginalAuthorAggregates -import com.twitter.ml.featurestore.catalog.features.timelines.TopicEngagementRealTimeAggregates -import com.twitter.ml.featurestore.catalog.features.timelines.TopicEngagementUserStateRealTimeAggregates -import com.twitter.ml.featurestore.catalog.features.timelines.TopicNegativeEngagementUserStateRealTimeAggregates -import com.twitter.ml.featurestore.catalog.features.timelines.UserEngagementAuthorUserStateRealTimeAggregates -import com.twitter.ml.featurestore.catalog.features.timelines.UserNegativeEngagementAuthorUserStateRealTimeAggregates -import com.twitter.ml.featurestore.lib.EntityId -import com.twitter.ml.featurestore.lib.UserId -import com.twitter.ml.featurestore.lib.feature.BoundFeature -import com.twitter.ml.featurestore.lib.feature.Feature - -object FeatureStoreFeatures { - import FeatureStoreRawFeatures._ - ///////////////////////////// Target user features //////////////////////// - val targetUserFeatures: Set[BoundFeature[_ <: EntityId, _]] = - (userKeyedFeatures ++ userAlgorithmAggregateFeatures).map(_.bind(UserEntity)) - - val targetUserResurrectionFeatures: Set[BoundFeature[_ <: EntityId, _]] = - userResurrectionFeatures.map(_.bind(UserEntity)) - val targetUserWtfImpressionFeatures: Set[BoundFeature[_ <: EntityId, _]] = - wtfImpressionUserFeatures.map(_.bind(UserEntity)) - val targetUserUserAuthorUserStateRealTimeAggregatesFeature: Set[BoundFeature[_ <: EntityId, _]] = - userAuthorUserStateRealTimeAggregatesFeature.map(_.bind(UserEntity)) - - val targetUserStatusFeatures: Set[BoundFeature[_ <: EntityId, _]] = - userStatusFeatures.map(_.bind(UserEntity).logarithm1p) - val targetUserMetricCountFeatures: Set[BoundFeature[_ <: EntityId, _]] = - mcFeatures.map(_.bind(UserEntity).logarithm1p) - - val targetUserClientFeatures: Set[BoundFeature[_ <: EntityId, _]] = - clientFeatures.map(_.bind(UserEntity)) - - ///////////////////////////// Candidate user features //////////////////////// - val candidateUserFeatures: Set[BoundFeature[_ <: EntityId, _]] = - userKeyedFeatures.map(_.bind(CandidateUserEntity)) - val candidateUserAuthorRealTimeAggregateFeatures: Set[BoundFeature[_ <: EntityId, _]] = - authorAggregateFeatures.map(_.bind(CandidateUserEntity)) - val candidateUserResurrectionFeatures: Set[BoundFeature[_ <: EntityId, _]] = - userResurrectionFeatures.map(_.bind(CandidateUserEntity)) - - val candidateUserStatusFeatures: Set[BoundFeature[_ <: EntityId, _]] = - userStatusFeatures.map(_.bind(CandidateUserEntity).logarithm1p) - val candidateUserTimelinesAuthorAggregateFeatures: Set[BoundFeature[_ <: EntityId, _]] = - Set(timelinesAuthorAggregateFeatures.bind(CandidateUserEntity)) - val candidateUserMetricCountFeatures: Set[BoundFeature[_ <: EntityId, _]] = - mcFeatures.map(_.bind(CandidateUserEntity).logarithm1p) - - val candidateUserClientFeatures: Set[BoundFeature[_ <: EntityId, _]] = - clientFeatures.map(_.bind(CandidateUserEntity)) - - val similarToUserFeatures: Set[BoundFeature[_ <: EntityId, _]] = - (userKeyedFeatures ++ authorAggregateFeatures).map(_.bind(AuthorEntity)) - - val similarToUserStatusFeatures: Set[BoundFeature[_ <: EntityId, _]] = - userStatusFeatures.map(_.bind(AuthorEntity).logarithm1p) - val similarToUserTimelinesAuthorAggregateFeatures: Set[BoundFeature[_ <: EntityId, _]] = - Set(timelinesAuthorAggregateFeatures.bind(AuthorEntity)) - val similarToUserMetricCountFeatures: Set[BoundFeature[_ <: EntityId, _]] = - mcFeatures.map(_.bind(AuthorEntity).logarithm1p) - - val userCandidateEdgeFeatures: Set[BoundFeature[_ <: EntityId, _]] = - (simclusterUVIntersectionFeatures ++ userCandidatePostNuxEdgeFeatures).map( - _.bind(UserCandidateEntity)) - val userCandidateWtfImpressionCandidateFeatures: Set[BoundFeature[_ <: EntityId, _]] = - wtfImpressionCandidateFeatures.map(_.bind(UserCandidateEntity)) - - /** - * Aggregate features based on candidate source algorithms. - */ - val postNuxAlgorithmIdAggregateFeatures: Set[BoundFeature[_ <: EntityId, _]] = - Set(PostNuxAlgorithmIdAggregateFeatureGroup.FeaturesAsDataRecord) - .map(_.bind(WtfAlgorithmIdEntity)) - - /** - * Aggregate features based on candidate source algorithm types. There are 4 at the moment: - * Geo, Social, Activity and Interest. - */ - val postNuxAlgorithmTypeAggregateFeatures: Set[BoundFeature[_ <: EntityId, _]] = - Set(PostNuxAlgorithmTypeAggregateFeatureGroup.FeaturesAsDataRecord) - .map(_.bind(WtfAlgorithmTypeEntity)) - - // user wtf-Algorithm features - val userWtfAlgorithmEdgeFeatures: Set[BoundFeature[_ <: EntityId, _]] = - FeatureGroupUtils.getTimelinesAggregationFrameworkCombinedFeatures( - UserWtfAlgorithmAggregate, - UserWtfAlgorithmEntity, - FeatureGroupUtils.getMaxSumAvgAggregate(UserWtfAlgorithmAggregate) - ) - - /** - * We have to add the max/sum/avg-aggregated features to the set of all features so that we can - * register them using FRS's [[FrsFeatureJsonExporter]]. - * - * Any additional such aggregated features that are included in [[FeatureStoreSource]] client - * should be registered here as well. - */ - val maxSumAvgAggregatedFeatureContext: FeatureContext = new FeatureContext() - .addFeatures( - UserWtfAlgorithmAggregate.getSecondaryAggregatedFeatureContext - ) - - // topic features - val topicAggregateFeatures: Set[BoundFeature[_ <: EntityId, _]] = Set( - TopicEngagementRealTimeAggregates.FeaturesAsDataRecord, - TopicNegativeEngagementUserStateRealTimeAggregates.FeaturesAsDataRecord, - TopicEngagementUserStateRealTimeAggregates.FeaturesAsDataRecord - ).map(_.bind(TopicEntity)) - val userTopicFeatures: Set[BoundFeature[_ <: EntityId, _]] = Set(FollowedTopics.bind(UserEntity)) - val authorTopicFeatures: Set[BoundFeature[_ <: EntityId, _]] = Set( - AuthorTopicAggregates.FeaturesAsDataRecord.bind(AuthorTopicEntity)) - val topicFeatures = topicAggregateFeatures ++ userTopicFeatures ++ authorTopicFeatures - -} - -object FeatureStoreRawFeatures { - val mcFeatures = Set( - NumTweets, - NumRetweets, - NumOriginalTweets, - NumRetweetsReceived, - NumFavoritesReceived, - NumRepliesReceived, - NumQuoteRetweetsReceived, - NumFollowsReceived, - NumFollowBacks, - NumFollows, - NumUnfollows, - NumUnfollowBacks, - NumQualityFollowReceived, - NumQuoteRetweets, - NumFavorites, - NumReplies, - NumLoginTweetImpressions, - NumTweetImpressions, - NumLoginDays, - NumUserActiveMinutes, - NumMuted, - NumSpamBlocked, - NumMuteBacks, - NumSpamBlockedBacks, - NumWasMutualFollowed, - NumWasMutualUnfollowed, - NumWasUnfollowed - ) - // based off usersource, and each feature represents the cumulative 'sent' counts - val userStatusFeatures = Set( - Favorites, - Followers, - Following, - Tweets - ) - // ratio features created from combining other features - val userRatioFeatures = Set(MutualFollowsFollowing, FollowersFollowings) - // features related to user login history - val userResurrectionFeatures: Set[Feature[UserId, Int]] = Set( - DaysSinceRecentResurrection - ) - - // real-time aggregate features borrowed from timelines - val authorAggregateFeatures = Set( - EngagementsReceivedByAuthorRealTimeAggregates.FeaturesAsDataRecord, - NegativeEngagementsReceivedByAuthorRealTimeAggregates.FeaturesAsDataRecord, - ) - - val timelinesAuthorAggregateFeatures = OriginalAuthorAggregates.FeaturesAsDataRecord - - val userAuthorUserStateRealTimeAggregatesFeature: Set[Feature[UserId, DataRecord]] = Set( - UserEngagementAuthorUserStateRealTimeAggregates.FeaturesAsDataRecord, - UserNegativeEngagementAuthorUserStateRealTimeAggregates.FeaturesAsDataRecord - ) - // post nux per-user offline features - val userOfflineFeatures = Set( - NumFollowings, - NumFollowers, - NumMutualFollows, - TweepCred, - UserState, - Language, - Country, - MutualFollowsOverFollowingRatio, - MutualFollowsOverFollowersRatio, - FollowersOverFollowingRatio, - ) - // matched post nux offline features between user and candidate - val userCandidatePostNuxEdgeFeatures = Set( - HaveSameUserState, - HaveSameLanguage, - HaveSameCountry, - NumFollowingsGap, - NumFollowersGap, - NumMutualFollowsGap, - TweepCredGap, - ) - // user algorithm aggregate features - val userAlgorithmAggregateFeatures = Set( - Impressions, - Follows, - FollowRate, - FollowsWithTweetImpressions, - FollowsWithTweetFavs, - FollowsTweetFavRate, - FollowsTweetReplies, - FollowsTweetReplyRate, - FollowsTweetRetweets, - FollowsTweetRetweetRate, - HasForwardEngagements, - HasReverseEngagements, - HasAnyEngagements, - ) - val userKeyedFeatures = userRatioFeatures ++ userOfflineFeatures - val wtfImpressionUserFeatures = - Set(HomeTimelineWtfCandidateCounts, HomeTimelineWtfLatestTimestamp) - val wtfImpressionCandidateFeatures = - Set(HomeTimelineWtfCandidateImpressionCounts, HomeTimelineWtfCandidateImpressionLatestTimestamp) - val simclusterUVIntersectionFeatures = Set( - IntersectionClusterIds, - HasIntersection, - IntersectionUserFollowScore, - IntersectionUserFavScore, - IntersectionCandidateKnownForScore, - IntersectionUserFollowCandidateKnownForScore, - IntersectionUserFavCandidateKnownForScore - ) - - // Client features - val clientFeatures = Set( - NumClients, - PrimaryClient, - PrimaryClientVersion, - FullPrimaryClientVersion, - PrimaryDeviceManufacturer, - PrimaryMobileSdkVersion, - SecondaryClient - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreGizmoduckSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreGizmoduckSource.scala deleted file mode 100644 index bb6b58857..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreGizmoduckSource.scala +++ /dev/null @@ -1,188 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.github.benmanes.caffeine.cache.Caffeine -import com.google.inject.Inject -import com.twitter.finagle.TimeoutException -import com.twitter.finagle.mtls.authentication.ServiceIdentifier -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSource -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSourceId -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.feature_hydration.sources.Utils.adaptAdditionalFeaturesToDataRecord -import com.twitter.follow_recommendations.common.feature_hydration.sources.Utils.randomizedTTL -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.ml.api.IRecordOneToOneAdapter -import com.twitter.ml.featurestore.catalog.datasets.core.UsersourceEntityDataset -import com.twitter.ml.featurestore.catalog.entities.core.{Author => AuthorEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{AuthorTopic => AuthorTopicEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{CandidateUser => CandidateUserEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{User => UserEntity} -import com.twitter.ml.featurestore.lib.EdgeEntityId -import com.twitter.ml.featurestore.lib.EntityId -import com.twitter.ml.featurestore.lib.TopicId -import com.twitter.ml.featurestore.lib.UserId -import com.twitter.ml.featurestore.lib.data.PredictionRecord -import com.twitter.ml.featurestore.lib.data.PredictionRecordAdapter -import com.twitter.ml.featurestore.lib.dataset.DatasetId -import com.twitter.ml.featurestore.lib.dataset.online.Hydrator.HydrationResponse -import com.twitter.ml.featurestore.lib.dataset.online.OnlineAccessDataset -import com.twitter.ml.featurestore.lib.dynamic.ClientConfig -import com.twitter.ml.featurestore.lib.dynamic.DynamicFeatureStoreClient -import com.twitter.ml.featurestore.lib.dynamic.DynamicHydrationConfig -import com.twitter.ml.featurestore.lib.dynamic.FeatureStoreParamsConfig -import com.twitter.ml.featurestore.lib.dynamic.GatedFeatures -import com.twitter.ml.featurestore.lib.feature.BoundFeature -import com.twitter.ml.featurestore.lib.feature.BoundFeatureSet -import com.twitter.ml.featurestore.lib.online.DatasetValuesCache -import com.twitter.ml.featurestore.lib.online.FeatureStoreRequest -import com.twitter.ml.featurestore.lib.online.OnlineFeatureGenerationStats -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import java.util.concurrent.TimeUnit -import com.twitter.conversions.DurationOps._ -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext - -class FeatureStoreGizmoduckSource @Inject() ( - serviceIdentifier: ServiceIdentifier, - stats: StatsReceiver) - extends FeatureSource { - import FeatureStoreGizmoduckSource._ - - val backupSourceStats = stats.scope("feature_store_hydration_gizmoduck") - val adapterStats = backupSourceStats.scope("adapters") - override def id: FeatureSourceId = FeatureSourceId.FeatureStoreGizmoduckSourceId - override def featureContext: FeatureContext = getFeatureContext - - val clientConfig: ClientConfig[HasParams] = ClientConfig( - dynamicHydrationConfig = dynamicHydrationConfig, - featureStoreParamsConfig = - FeatureStoreParamsConfig(FeatureStoreParameters.featureStoreParams, Map.empty), - /** - * The smaller one between `timeoutProvider` and `FeatureStoreSourceParams.GlobalFetchTimeout` - * used below takes effect. - */ - timeoutProvider = Function.const(800.millis), - serviceIdentifier = serviceIdentifier - ) - - private val datasetsToCache = Set( - UsersourceEntityDataset - ).asInstanceOf[Set[OnlineAccessDataset[_ <: EntityId, _]]] - - private val datasetValuesCache: DatasetValuesCache = - DatasetValuesCache( - Caffeine - .newBuilder() - .expireAfterWrite(randomizedTTL(12.hours.inSeconds), TimeUnit.SECONDS) - .maximumSize(DefaultCacheMaxKeys) - .build[(_ <: EntityId, DatasetId), Stitch[HydrationResponse[_]]] - .asMap, - datasetsToCache, - DatasetCacheScope - ) - - private val dynamicFeatureStoreClient = DynamicFeatureStoreClient( - clientConfig, - backupSourceStats, - Set(datasetValuesCache) - ) - - private val adapter: IRecordOneToOneAdapter[PredictionRecord] = - PredictionRecordAdapter.oneToOne( - BoundFeatureSet(allFeatures), - OnlineFeatureGenerationStats(backupSourceStats) - ) - - override def hydrateFeatures( - target: HasClientContext - with HasPreFetchedFeature - with HasParams - with HasSimilarToContext - with HasDisplayLocation, - candidates: Seq[CandidateUser] - ): Stitch[Map[CandidateUser, DataRecord]] = { - target.getOptionalUserId - .map { targetUserId => - val featureRequests = candidates.map { candidate => - val userEntityId = UserEntity.withId(UserId(targetUserId)) - val candidateEntityId = CandidateUserEntity.withId(UserId(candidate.id)) - val similarToUserId = target.similarToUserIds.map(id => AuthorEntity.withId(UserId(id))) - val topicProof = candidate.reason.flatMap(_.accountProof.flatMap(_.topicProof)) - val authorTopicEntity = if (topicProof.isDefined) { - backupSourceStats.counter("candidates_with_topic_proof").incr() - Set( - AuthorTopicEntity.withId( - EdgeEntityId(UserId(candidate.id), TopicId(topicProof.get.topicId)))) - } else Nil - - val entities = - Seq(userEntityId, candidateEntityId) ++ similarToUserId ++ authorTopicEntity - FeatureStoreRequest(entities) - } - - val predictionRecordsFut = dynamicFeatureStoreClient(featureRequests, target) - val candidateFeatureMap = predictionRecordsFut.map { predictionRecords => - // we can zip predictionRecords with candidates as the order is preserved in the client - candidates - .zip(predictionRecords).map { - case (candidate, predictionRecord) => - candidate -> adaptAdditionalFeaturesToDataRecord( - adapter.adaptToDataRecord(predictionRecord), - adapterStats, - FeatureStoreSource.featureAdapters) - }.toMap - } - Stitch - .callFuture(candidateFeatureMap) - .within(target.params(FeatureStoreSourceParams.GlobalFetchTimeout))( - com.twitter.finagle.util.DefaultTimer) - .rescue { - case _: TimeoutException => - Stitch.value(Map.empty[CandidateUser, DataRecord]) - } - }.getOrElse(Stitch.value(Map.empty[CandidateUser, DataRecord])) - } -} - -object FeatureStoreGizmoduckSource { - private val DatasetCacheScope = "feature_store_local_cache_gizmoduck" - private val DefaultCacheMaxKeys = 20000 - - val allFeatures: Set[BoundFeature[_ <: EntityId, _]] = - FeatureStoreFeatures.candidateUserStatusFeatures ++ - FeatureStoreFeatures.similarToUserStatusFeatures ++ - FeatureStoreFeatures.targetUserStatusFeatures - - val getFeatureContext: FeatureContext = - BoundFeatureSet(allFeatures).toFeatureContext - - val dynamicHydrationConfig: DynamicHydrationConfig[HasParams] = - DynamicHydrationConfig( - Set( - GatedFeatures( - boundFeatureSet = BoundFeatureSet(FeatureStoreFeatures.targetUserStatusFeatures), - gate = HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForGizmoduck) & - HasParams.paramGate(FeatureStoreSourceParams.EnableTargetUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(FeatureStoreFeatures.candidateUserStatusFeatures), - gate = - HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForGizmoduck) & - HasParams.paramGate(FeatureStoreSourceParams.EnableCandidateUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(FeatureStoreFeatures.similarToUserStatusFeatures), - gate = - HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForGizmoduck) & - HasParams.paramGate(FeatureStoreSourceParams.EnableSimilarToUserFeatures) - ), - )) - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreParameters.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreParameters.scala deleted file mode 100644 index 468a15ff6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreParameters.scala +++ /dev/null @@ -1,79 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.twitter.conversions.DurationOps._ -import com.twitter.ml.featurestore.catalog.datasets.core.UserMobileSdkDataset -import com.twitter.ml.featurestore.catalog.datasets.core.UsersourceEntityDataset -import com.twitter.ml.featurestore.catalog.datasets.customer_journey.PostNuxAlgorithmIdAggregateDataset -import com.twitter.ml.featurestore.catalog.datasets.customer_journey.PostNuxAlgorithmTypeAggregateDataset -import com.twitter.ml.featurestore.catalog.datasets.magicrecs.NotificationSummariesEntityDataset -import com.twitter.ml.featurestore.catalog.datasets.onboarding.MetricCenterUserCountingFeaturesDataset -import com.twitter.ml.featurestore.catalog.datasets.onboarding.UserWtfAlgorithmAggregateFeaturesDataset -import com.twitter.ml.featurestore.catalog.datasets.onboarding.WhoToFollowPostNuxFeaturesDataset -import com.twitter.ml.featurestore.catalog.datasets.rux.UserRecentReactivationTimeDataset -import com.twitter.ml.featurestore.catalog.datasets.timelines.AuthorFeaturesEntityDataset -import com.twitter.ml.featurestore.lib.dataset.DatasetParams -import com.twitter.ml.featurestore.lib.dataset.online.BatchingPolicy -import com.twitter.ml.featurestore.lib.params.FeatureStoreParams -import com.twitter.strato.opcontext.Attribution.ManhattanAppId -import com.twitter.strato.opcontext.ServeWithin - -object FeatureStoreParameters { - - private val FeatureServiceBatchSize = 100 - - val featureStoreParams = FeatureStoreParams( - global = DatasetParams( - serveWithin = Some(ServeWithin(duration = 240.millis, roundTripAllowance = None)), - attributions = Seq( - ManhattanAppId("omega", "wtf_impression_store"), - ManhattanAppId("athena", "wtf_athena"), - ManhattanAppId("starbuck", "wtf_starbuck"), - ManhattanAppId("apollo", "wtf_apollo") - ), - batchingPolicy = Some(BatchingPolicy.Isolated(FeatureServiceBatchSize)) - ), - perDataset = Map( - MetricCenterUserCountingFeaturesDataset.id -> - DatasetParams( - stratoSuffix = Some("onboarding"), - batchingPolicy = Some(BatchingPolicy.Isolated(200)) - ), - UsersourceEntityDataset.id -> - DatasetParams( - stratoSuffix = Some("onboarding") - ), - WhoToFollowPostNuxFeaturesDataset.id -> - DatasetParams( - stratoSuffix = Some("onboarding"), - batchingPolicy = Some(BatchingPolicy.Isolated(200)) - ), - AuthorFeaturesEntityDataset.id -> - DatasetParams( - stratoSuffix = Some("onboarding"), - batchingPolicy = Some(BatchingPolicy.Isolated(10)) - ), - UserRecentReactivationTimeDataset.id -> DatasetParams( - stratoSuffix = - None // removed due to low hit rate. we should use a negative cache in the future - ), - UserWtfAlgorithmAggregateFeaturesDataset.id -> DatasetParams( - stratoSuffix = None - ), - NotificationSummariesEntityDataset.id -> DatasetParams( - stratoSuffix = Some("onboarding"), - serveWithin = Some(ServeWithin(duration = 45.millis, roundTripAllowance = None)), - batchingPolicy = Some(BatchingPolicy.Isolated(10)) - ), - UserMobileSdkDataset.id -> DatasetParams( - stratoSuffix = Some("onboarding") - ), - PostNuxAlgorithmIdAggregateDataset.id -> DatasetParams( - stratoSuffix = Some("onboarding") - ), - PostNuxAlgorithmTypeAggregateDataset.id -> DatasetParams( - stratoSuffix = Some("onboarding") - ), - ), - enableFeatureGenerationStats = true - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStorePostNuxAlgorithmSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStorePostNuxAlgorithmSource.scala deleted file mode 100644 index 6821e6a5f..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStorePostNuxAlgorithmSource.scala +++ /dev/null @@ -1,232 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.github.benmanes.caffeine.cache.Caffeine -import com.google.inject.Inject -import com.twitter.conversions.DurationOps._ -import com.twitter.finagle.TimeoutException -import com.twitter.finagle.mtls.authentication.ServiceIdentifier -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.constants.CandidateAlgorithmTypeConstants -import com.twitter.follow_recommendations.common.feature_hydration.adapters.CandidateAlgorithmAdapter.remapCandidateSource -import com.twitter.follow_recommendations.common.feature_hydration.adapters.PostNuxAlgorithmIdAdapter -import com.twitter.follow_recommendations.common.feature_hydration.adapters.PostNuxAlgorithmTypeAdapter -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSource -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSourceId -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.feature_hydration.sources.Utils.adaptAdditionalFeaturesToDataRecord -import com.twitter.follow_recommendations.common.feature_hydration.sources.Utils.randomizedTTL -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.hermit.constants.AlgorithmFeedbackTokens.AlgorithmToFeedbackTokenMap -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.DataRecordMerger -import com.twitter.ml.api.FeatureContext -import com.twitter.ml.api.IRecordOneToOneAdapter -import com.twitter.ml.featurestore.catalog.datasets.customer_journey.PostNuxAlgorithmIdAggregateDataset -import com.twitter.ml.featurestore.catalog.datasets.customer_journey.PostNuxAlgorithmTypeAggregateDataset -import com.twitter.ml.featurestore.catalog.entities.onboarding.{WtfAlgorithm => OnboardingWtfAlgoId} -import com.twitter.ml.featurestore.catalog.entities.onboarding.{ - WtfAlgorithmType => OnboardingWtfAlgoType -} -import com.twitter.ml.featurestore.catalog.features.customer_journey.CombineAllFeaturesPolicy -import com.twitter.ml.featurestore.lib.EntityId -import com.twitter.ml.featurestore.lib.WtfAlgorithmId -import com.twitter.ml.featurestore.lib.WtfAlgorithmType -import com.twitter.ml.featurestore.lib.data.PredictionRecord -import com.twitter.ml.featurestore.lib.data.PredictionRecordAdapter -import com.twitter.ml.featurestore.lib.dataset.DatasetId -import com.twitter.ml.featurestore.lib.dataset.online.Hydrator.HydrationResponse -import com.twitter.ml.featurestore.lib.dataset.online.OnlineAccessDataset -import com.twitter.ml.featurestore.lib.dynamic.ClientConfig -import com.twitter.ml.featurestore.lib.dynamic.DynamicFeatureStoreClient -import com.twitter.ml.featurestore.lib.dynamic.DynamicHydrationConfig -import com.twitter.ml.featurestore.lib.dynamic.FeatureStoreParamsConfig -import com.twitter.ml.featurestore.lib.dynamic.GatedFeatures -import com.twitter.ml.featurestore.lib.entity.EntityWithId -import com.twitter.ml.featurestore.lib.feature.BoundFeature -import com.twitter.ml.featurestore.lib.feature.BoundFeatureSet -import com.twitter.ml.featurestore.lib.online.DatasetValuesCache -import com.twitter.ml.featurestore.lib.online.FeatureStoreRequest -import com.twitter.ml.featurestore.lib.online.OnlineFeatureGenerationStats -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import java.util.concurrent.TimeUnit -import scala.collection.JavaConverters._ - -class FeatureStorePostNuxAlgorithmSource @Inject() ( - serviceIdentifier: ServiceIdentifier, - stats: StatsReceiver) - extends FeatureSource { - import FeatureStorePostNuxAlgorithmSource._ - - val backupSourceStats = stats.scope("feature_store_hydration_post_nux_algorithm") - val adapterStats = backupSourceStats.scope("adapters") - override def id: FeatureSourceId = FeatureSourceId.FeatureStorePostNuxAlgorithmSourceId - override def featureContext: FeatureContext = getFeatureContext - - private val dataRecordMerger = new DataRecordMerger - - val clientConfig: ClientConfig[HasParams] = ClientConfig( - dynamicHydrationConfig = dynamicHydrationConfig, - featureStoreParamsConfig = - FeatureStoreParamsConfig(FeatureStoreParameters.featureStoreParams, Map.empty), - /** - * The smaller one between `timeoutProvider` and `FeatureStoreSourceParams.GlobalFetchTimeout` - * used below takes effect. - */ - timeoutProvider = Function.const(800.millis), - serviceIdentifier = serviceIdentifier - ) - - private val datasetsToCache = Set( - PostNuxAlgorithmIdAggregateDataset, - PostNuxAlgorithmTypeAggregateDataset, - ).asInstanceOf[Set[OnlineAccessDataset[_ <: EntityId, _]]] - - private val datasetValuesCache: DatasetValuesCache = - DatasetValuesCache( - Caffeine - .newBuilder() - .expireAfterWrite(randomizedTTL(12.hours.inSeconds), TimeUnit.SECONDS) - .maximumSize(DefaultCacheMaxKeys) - .build[(_ <: EntityId, DatasetId), Stitch[HydrationResponse[_]]] - .asMap, - datasetsToCache, - DatasetCacheScope - ) - - private val dynamicFeatureStoreClient = DynamicFeatureStoreClient( - clientConfig, - backupSourceStats, - Set(datasetValuesCache) - ) - - private val adapterToDataRecord: IRecordOneToOneAdapter[PredictionRecord] = - PredictionRecordAdapter.oneToOne( - BoundFeatureSet(allFeatures), - OnlineFeatureGenerationStats(backupSourceStats) - ) - - // These two calculate the rate for each feature by dividing it by the number of impressions, then - // apply a log transformation. - private val transformAdapters = Seq(PostNuxAlgorithmIdAdapter, PostNuxAlgorithmTypeAdapter) - override def hydrateFeatures( - target: HasClientContext - with HasPreFetchedFeature - with HasParams - with HasSimilarToContext - with HasDisplayLocation, - candidates: Seq[CandidateUser] - ): Stitch[Map[CandidateUser, DataRecord]] = { - target.getOptionalUserId - .map { _: Long => - val candidateAlgoIdEntities = candidates.map { candidate => - candidate.id -> candidate.getAllAlgorithms - .flatMap { algo => - AlgorithmToFeedbackTokenMap.get(remapCandidateSource(algo)) - }.map(algoId => OnboardingWtfAlgoId.withId(WtfAlgorithmId(algoId))) - }.toMap - - val candidateAlgoTypeEntities = candidateAlgoIdEntities.map { - case (candidateId, algoIdEntities) => - candidateId -> algoIdEntities - .map(_.id.algoId) - .flatMap(algoId => CandidateAlgorithmTypeConstants.getAlgorithmTypes(algoId.toString)) - .distinct - .map(algoType => OnboardingWtfAlgoType.withId(WtfAlgorithmType(algoType))) - } - - val entities = { - candidateAlgoIdEntities.values.flatten ++ candidateAlgoTypeEntities.values.flatten - }.toSeq.distinct - val requests = entities.map(entity => FeatureStoreRequest(Seq(entity))) - - val predictionRecordsFut = dynamicFeatureStoreClient(requests, target) - val candidateFeatureMap = predictionRecordsFut.map { - predictionRecords: Seq[PredictionRecord] => - val entityFeatureMap: Map[EntityWithId[_], DataRecord] = entities - .zip(predictionRecords).map { - case (entity, predictionRecord) => - entity -> adaptAdditionalFeaturesToDataRecord( - adapterToDataRecord.adaptToDataRecord(predictionRecord), - adapterStats, - transformAdapters) - }.toMap - - // In case we have more than one algorithm ID, or type, for a candidate, we merge the - // resulting DataRecords using the two merging policies below. - val algoIdMergeFn = - CombineAllFeaturesPolicy(PostNuxAlgorithmIdAdapter.getFeatures).getMergeFn - val algoTypeMergeFn = - CombineAllFeaturesPolicy(PostNuxAlgorithmTypeAdapter.getFeatures).getMergeFn - - val candidateAlgoIdFeaturesMap = candidateAlgoIdEntities.mapValues { entities => - val features = entities.flatMap(e => Option(entityFeatureMap.getOrElse(e, null))) - algoIdMergeFn(features) - } - - val candidateAlgoTypeFeaturesMap = candidateAlgoTypeEntities.mapValues { entities => - val features = entities.flatMap(e => Option(entityFeatureMap.getOrElse(e, null))) - algoTypeMergeFn(features) - } - - candidates.map { candidate => - val idDrOpt = candidateAlgoIdFeaturesMap.getOrElse(candidate.id, None) - val typeDrOpt = candidateAlgoTypeFeaturesMap.getOrElse(candidate.id, None) - - val featureDr = (idDrOpt, typeDrOpt) match { - case (None, Some(typeDataRecord)) => typeDataRecord - case (Some(idDataRecord), None) => idDataRecord - case (None, None) => new DataRecord() - case (Some(idDataRecord), Some(typeDataRecord)) => - dataRecordMerger.merge(idDataRecord, typeDataRecord) - idDataRecord - } - candidate -> featureDr - }.toMap - } - Stitch - .callFuture(candidateFeatureMap) - .within(target.params(FeatureStoreSourceParams.GlobalFetchTimeout))( - com.twitter.finagle.util.DefaultTimer) - .rescue { - case _: TimeoutException => - Stitch.value(Map.empty[CandidateUser, DataRecord]) - } - }.getOrElse(Stitch.value(Map.empty[CandidateUser, DataRecord])) - } -} - -object FeatureStorePostNuxAlgorithmSource { - private val DatasetCacheScope = "feature_store_local_cache_post_nux_algorithm" - private val DefaultCacheMaxKeys = 1000 // Both of these datasets have <50 keys total. - - val allFeatures: Set[BoundFeature[_ <: EntityId, _]] = - FeatureStoreFeatures.postNuxAlgorithmIdAggregateFeatures ++ - FeatureStoreFeatures.postNuxAlgorithmTypeAggregateFeatures - - val algoIdFinalFeatures = CombineAllFeaturesPolicy( - PostNuxAlgorithmIdAdapter.getFeatures).outputFeaturesPostMerge.toSeq - val algoTypeFinalFeatures = CombineAllFeaturesPolicy( - PostNuxAlgorithmTypeAdapter.getFeatures).outputFeaturesPostMerge.toSeq - - val getFeatureContext: FeatureContext = - new FeatureContext().addFeatures((algoIdFinalFeatures ++ algoTypeFinalFeatures).asJava) - - val dynamicHydrationConfig: DynamicHydrationConfig[HasParams] = - DynamicHydrationConfig( - Set( - GatedFeatures( - boundFeatureSet = - BoundFeatureSet(FeatureStoreFeatures.postNuxAlgorithmIdAggregateFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableAlgorithmAggregateFeatures) - ), - GatedFeatures( - boundFeatureSet = - BoundFeatureSet(FeatureStoreFeatures.postNuxAlgorithmTypeAggregateFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableAlgorithmAggregateFeatures) - ), - )) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreSource.scala deleted file mode 100644 index 991bdee86..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreSource.scala +++ /dev/null @@ -1,368 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.github.benmanes.caffeine.cache.Caffeine -import com.google.inject.Inject -import com.google.inject.Singleton -import com.twitter.conversions.DurationOps._ -import com.twitter.finagle.TimeoutException -import com.twitter.finagle.mtls.authentication.ServiceIdentifier -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.feature_hydration.adapters.CandidateAlgorithmAdapter.remapCandidateSource -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSource -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSourceId -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.feature_hydration.sources.Utils.adaptAdditionalFeaturesToDataRecord -import com.twitter.follow_recommendations.common.feature_hydration.sources.Utils.randomizedTTL -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.hermit.constants.AlgorithmFeedbackTokens.AlgorithmToFeedbackTokenMap -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.ml.api.IRecordOneToOneAdapter -import com.twitter.ml.featurestore.catalog.datasets.core.UsersourceEntityDataset -import com.twitter.ml.featurestore.catalog.datasets.magicrecs.NotificationSummariesEntityDataset -import com.twitter.ml.featurestore.catalog.datasets.onboarding.MetricCenterUserCountingFeaturesDataset -import com.twitter.ml.featurestore.catalog.datasets.timelines.AuthorFeaturesEntityDataset -import com.twitter.ml.featurestore.catalog.entities.core.{Author => AuthorEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{AuthorTopic => AuthorTopicEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{CandidateUser => CandidateUserEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{Topic => TopicEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{User => UserEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{UserCandidate => UserCandidateEntity} -import com.twitter.ml.featurestore.catalog.entities.onboarding.UserWtfAlgorithmEntity -import com.twitter.ml.featurestore.lib.data.PredictionRecord -import com.twitter.ml.featurestore.lib.data.PredictionRecordAdapter -import com.twitter.ml.featurestore.lib.dataset.online.Hydrator.HydrationResponse -import com.twitter.ml.featurestore.lib.dataset.online.OnlineAccessDataset -import com.twitter.ml.featurestore.lib.dataset.DatasetId -import com.twitter.ml.featurestore.lib.dynamic._ -import com.twitter.ml.featurestore.lib.feature._ -import com.twitter.ml.featurestore.lib.online.DatasetValuesCache -import com.twitter.ml.featurestore.lib.online.FeatureStoreRequest -import com.twitter.ml.featurestore.lib.online.OnlineFeatureGenerationStats -import com.twitter.ml.featurestore.lib.EdgeEntityId -import com.twitter.ml.featurestore.lib.EntityId -import com.twitter.ml.featurestore.lib.TopicId -import com.twitter.ml.featurestore.lib.UserId -import com.twitter.ml.featurestore.lib.WtfAlgorithmId -import com.twitter.onboarding.relevance.adapters.features.featurestore.CandidateAuthorTopicAggregatesAdapter -import com.twitter.onboarding.relevance.adapters.features.featurestore.CandidateTopicEngagementRealTimeAggregatesAdapter -import com.twitter.onboarding.relevance.adapters.features.featurestore.CandidateTopicEngagementUserStateRealTimeAggregatesAdapter -import com.twitter.onboarding.relevance.adapters.features.featurestore.CandidateTopicNegativeEngagementUserStateRealTimeAggregatesAdapter -import com.twitter.onboarding.relevance.adapters.features.featurestore.FeatureStoreAdapter -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -import java.util.concurrent.TimeUnit - -@Singleton -class FeatureStoreSource @Inject() ( - serviceIdentifier: ServiceIdentifier, - stats: StatsReceiver) - extends FeatureSource { - import FeatureStoreSource._ - - override val id: FeatureSourceId = FeatureSourceId.FeatureStoreSourceId - override val featureContext: FeatureContext = FeatureStoreSource.getFeatureContext - val hydrateFeaturesStats = stats.scope("hydrate_features") - val adapterStats = stats.scope("adapters") - val featureSet: BoundFeatureSet = BoundFeatureSet(FeatureStoreSource.allFeatures) - val clientConfig: ClientConfig[HasParams] = ClientConfig( - dynamicHydrationConfig = FeatureStoreSource.dynamicHydrationConfig, - featureStoreParamsConfig = - FeatureStoreParamsConfig(FeatureStoreParameters.featureStoreParams, Map.empty), - /** - * The smaller one between `timeoutProvider` and `FeatureStoreSourceParams.GlobalFetchTimeout` - * used below takes effect. - */ - timeoutProvider = Function.const(800.millis), - serviceIdentifier = serviceIdentifier - ) - - private val datasetsToCache = Set( - MetricCenterUserCountingFeaturesDataset, - UsersourceEntityDataset, - AuthorFeaturesEntityDataset, - NotificationSummariesEntityDataset - ).asInstanceOf[Set[OnlineAccessDataset[_ <: EntityId, _]]] - - private val datasetValuesCache: DatasetValuesCache = - DatasetValuesCache( - Caffeine - .newBuilder() - .expireAfterWrite(randomizedTTL(12.hours.inSeconds), TimeUnit.SECONDS) - .maximumSize(DefaultCacheMaxKeys) - .build[(_ <: EntityId, DatasetId), Stitch[HydrationResponse[_]]] - .asMap, - datasetsToCache, - DatasetCacheScope - ) - - private val dynamicFeatureStoreClient = DynamicFeatureStoreClient( - clientConfig, - stats, - Set(datasetValuesCache) - ) - - private val adapter: IRecordOneToOneAdapter[PredictionRecord] = - PredictionRecordAdapter.oneToOne( - BoundFeatureSet(allFeatures), - OnlineFeatureGenerationStats(stats) - ) - - override def hydrateFeatures( - target: HasClientContext - with HasPreFetchedFeature - with HasParams - with HasSimilarToContext - with HasDisplayLocation, - candidates: Seq[CandidateUser] - ): Stitch[Map[CandidateUser, DataRecord]] = { - target.getOptionalUserId - .map { targetUserId => - val featureRequests = candidates.map { candidate => - val userId = UserId(targetUserId) - val userEntityId = UserEntity.withId(userId) - val candidateEntityId = CandidateUserEntity.withId(UserId(candidate.id)) - val userCandidateEdgeEntityId = - UserCandidateEntity.withId(EdgeEntityId(userId, UserId(candidate.id))) - val similarToUserId = target.similarToUserIds.map(id => AuthorEntity.withId(UserId(id))) - val topicProof = candidate.reason.flatMap(_.accountProof.flatMap(_.topicProof)) - val topicEntities = if (topicProof.isDefined) { - hydrateFeaturesStats.counter("candidates_with_topic_proof").incr() - val topicId = topicProof.get.topicId - val topicEntityId = TopicEntity.withId(TopicId(topicId)) - val authorTopicEntityId = - AuthorTopicEntity.withId(EdgeEntityId(UserId(candidate.id), TopicId(topicId))) - Seq(topicEntityId, authorTopicEntityId) - } else Nil - - val candidateAlgorithmsWithScores = candidate.getAllAlgorithms - val userWtfAlgEdgeEntities = - candidateAlgorithmsWithScores.flatMap(algo => { - val algoId = AlgorithmToFeedbackTokenMap.get(remapCandidateSource(algo)) - algoId.map(id => - UserWtfAlgorithmEntity.withId(EdgeEntityId(userId, WtfAlgorithmId(id)))) - }) - - val entities = Seq( - userEntityId, - candidateEntityId, - userCandidateEdgeEntityId) ++ similarToUserId ++ topicEntities ++ userWtfAlgEdgeEntities - FeatureStoreRequest(entities) - } - - val predictionRecordsFut = dynamicFeatureStoreClient(featureRequests, target) - val candidateFeatureMap = predictionRecordsFut.map { predictionRecords => - // we can zip predictionRecords with candidates as the order is preserved in the client - candidates - .zip(predictionRecords).map { - case (candidate, predictionRecord) => - candidate -> adaptAdditionalFeaturesToDataRecord( - adapter.adaptToDataRecord(predictionRecord), - adapterStats, - FeatureStoreSource.featureAdapters) - }.toMap - } - Stitch - .callFuture(candidateFeatureMap) - .within(target.params(FeatureStoreSourceParams.GlobalFetchTimeout))( - com.twitter.finagle.util.DefaultTimer) - .rescue { - case _: TimeoutException => - Stitch.value(Map.empty[CandidateUser, DataRecord]) - } - }.getOrElse(Stitch.value(Map.empty[CandidateUser, DataRecord])) - } -} - -// list of features that we will be fetching, even if we are only scribing but not scoring with them -object FeatureStoreSource { - - private val DatasetCacheScope = "feature_store_local_cache" - private val DefaultCacheMaxKeys = 70000 - - import FeatureStoreFeatures._ - - ///////////////////// ALL hydrated features ///////////////////// - val allFeatures: Set[BoundFeature[_ <: EntityId, _]] = - //target user - targetUserFeatures ++ - targetUserUserAuthorUserStateRealTimeAggregatesFeature ++ - targetUserResurrectionFeatures ++ - targetUserWtfImpressionFeatures ++ - targetUserStatusFeatures ++ - targetUserMetricCountFeatures ++ - //candidate user - candidateUserFeatures ++ - candidateUserResurrectionFeatures ++ - candidateUserAuthorRealTimeAggregateFeatures ++ - candidateUserStatusFeatures ++ - candidateUserMetricCountFeatures ++ - candidateUserTimelinesAuthorAggregateFeatures ++ - candidateUserClientFeatures ++ - //similar to user - similarToUserFeatures ++ - similarToUserStatusFeatures ++ - similarToUserMetricCountFeatures ++ - similarToUserTimelinesAuthorAggregateFeatures ++ - //other - userCandidateEdgeFeatures ++ - userCandidateWtfImpressionCandidateFeatures ++ - topicFeatures ++ - userWtfAlgorithmEdgeFeatures ++ - targetUserClientFeatures - - val dynamicHydrationConfig: DynamicHydrationConfig[HasParams] = - DynamicHydrationConfig( - Set( - GatedFeatures( - boundFeatureSet = BoundFeatureSet(topicAggregateFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableTopicAggregateFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(authorTopicFeatures), - gate = - HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForTimelinesAuthors).unary_! & - HasParams.paramGate(FeatureStoreSourceParams.EnableAuthorTopicAggregateFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(userTopicFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableUserTopicFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(targetUserFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableTargetUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(targetUserUserAuthorUserStateRealTimeAggregatesFeature), - gate = HasParams.paramGate( - FeatureStoreSourceParams.EnableTargetUserUserAuthorUserStateRealTimeAggregatesFeature) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(targetUserResurrectionFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableTargetUserResurrectionFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(targetUserWtfImpressionFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableTargetUserWtfImpressionFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(targetUserStatusFeatures), - gate = - HasParams.paramGate(FeatureStoreSourceParams.EnableSeparateClientForGizmoduck).unary_! & - HasParams.paramGate(FeatureStoreSourceParams.EnableTargetUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(targetUserMetricCountFeatures), - gate = HasParams - .paramGate( - FeatureStoreSourceParams.EnableSeparateClientForMetricCenterUserCounting).unary_! & - HasParams.paramGate(FeatureStoreSourceParams.EnableTargetUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(candidateUserFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableCandidateUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(candidateUserAuthorRealTimeAggregateFeatures), - gate = HasParams.paramGate( - FeatureStoreSourceParams.EnableCandidateUserAuthorRealTimeAggregateFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(candidateUserResurrectionFeatures), - gate = - HasParams.paramGate(FeatureStoreSourceParams.EnableCandidateUserResurrectionFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(candidateUserStatusFeatures), - gate = - HasParams.paramGate(FeatureStoreSourceParams.EnableSeparateClientForGizmoduck).unary_! & - HasParams.paramGate(FeatureStoreSourceParams.EnableCandidateUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(candidateUserTimelinesAuthorAggregateFeatures), - gate = - HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForTimelinesAuthors).unary_! & - HasParams.paramGate( - FeatureStoreSourceParams.EnableCandidateUserTimelinesAuthorAggregateFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(candidateUserMetricCountFeatures), - gate = - HasParams - .paramGate( - FeatureStoreSourceParams.EnableSeparateClientForMetricCenterUserCounting).unary_! & - HasParams.paramGate(FeatureStoreSourceParams.EnableCandidateUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(userCandidateEdgeFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableUserCandidateEdgeFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(userCandidateWtfImpressionCandidateFeatures), - gate = HasParams.paramGate( - FeatureStoreSourceParams.EnableUserCandidateWtfImpressionCandidateFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(userWtfAlgorithmEdgeFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableUserWtfAlgEdgeFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(similarToUserFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableSimilarToUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(similarToUserStatusFeatures), - gate = - HasParams.paramGate(FeatureStoreSourceParams.EnableSeparateClientForGizmoduck).unary_! & - HasParams.paramGate(FeatureStoreSourceParams.EnableSimilarToUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(similarToUserTimelinesAuthorAggregateFeatures), - gate = - HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForTimelinesAuthors).unary_! & - HasParams.paramGate(FeatureStoreSourceParams.EnableSimilarToUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(similarToUserMetricCountFeatures), - gate = - HasParams - .paramGate( - FeatureStoreSourceParams.EnableSeparateClientForMetricCenterUserCounting).unary_! & - HasParams.paramGate(FeatureStoreSourceParams.EnableSimilarToUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(candidateUserClientFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableCandidateClientFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(targetUserClientFeatures), - gate = HasParams.paramGate(FeatureStoreSourceParams.EnableUserClientFeatures) - ), - ) - ) - // for calibrating features, e.g. add log transformed topic features - val featureAdapters: Seq[FeatureStoreAdapter] = Seq( - CandidateTopicEngagementRealTimeAggregatesAdapter, - CandidateTopicNegativeEngagementUserStateRealTimeAggregatesAdapter, - CandidateTopicEngagementUserStateRealTimeAggregatesAdapter, - CandidateAuthorTopicAggregatesAdapter - ) - val additionalFeatureContext: FeatureContext = FeatureContext.merge( - featureAdapters - .foldRight(new FeatureContext())((adapter, context) => - context - .addFeatures(adapter.getFeatureContext)) - ) - val getFeatureContext: FeatureContext = - BoundFeatureSet(allFeatures).toFeatureContext - .addFeatures(additionalFeatureContext) - // The below are aggregated features that are aggregated for a second time over multiple keys. - .addFeatures(maxSumAvgAggregatedFeatureContext) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreSourceParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreSourceParams.scala deleted file mode 100644 index 15488ce90..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreSourceParams.scala +++ /dev/null @@ -1,148 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.twitter.timelines.configapi.DurationConversion -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.util.Duration -import com.twitter.conversions.DurationOps._ - -object FeatureStoreSourceParams { - case object EnableTopicAggregateFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableTopicAggregateFeatures, - default = true - ) - case object EnableAlgorithmAggregateFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableAlgorithmAggregateFeatures, - default = false - ) - case object EnableAuthorTopicAggregateFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableAuthorTopicAggregateFeatures, - default = true - ) - case object EnableUserTopicFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableUserTopicFeatures, - default = false - ) - case object EnableTargetUserFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableTargetUserFeatures, - default = true - ) - case object EnableTargetUserUserAuthorUserStateRealTimeAggregatesFeature - extends FSParam[Boolean]( - name = - FeatureHydrationSourcesFeatureSwitchKeys.EnableTargetUserUserAuthorUserStateRealTimeAggregatesFeature, - default = true - ) - case object EnableTargetUserResurrectionFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableTargetUserResurrectionFeatures, - default = true - ) - case object EnableTargetUserWtfImpressionFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableTargetUserWtfImpressionFeatures, - default = true - ) - case object EnableCandidateUserFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableCandidateUserFeatures, - default = true - ) - case object EnableCandidateUserAuthorRealTimeAggregateFeatures - extends FSParam[Boolean]( - name = - FeatureHydrationSourcesFeatureSwitchKeys.EnableCandidateUserAuthorRealTimeAggregateFeatures, - default = true - ) - case object EnableCandidateUserResurrectionFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableCandidateUserResurrectionFeatures, - default = true - ) - case object EnableCandidateUserTimelinesAuthorAggregateFeatures - extends FSParam[Boolean]( - name = - FeatureHydrationSourcesFeatureSwitchKeys.EnableCandidateUserTimelinesAuthorAggregateFeatures, - default = true - ) - case object EnableUserCandidateEdgeFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableUserCandidateEdgeFeatures, - default = true - ) - case object EnableUserCandidateWtfImpressionCandidateFeatures - extends FSParam[Boolean]( - name = - FeatureHydrationSourcesFeatureSwitchKeys.EnableUserCandidateWtfImpressionCandidateFeatures, - default = true - ) - case object EnableUserWtfAlgEdgeFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableUserWtfAlgEdgeFeatures, - default = false - ) - case object EnableSimilarToUserFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableSimilarToUserFeatures, - default = true - ) - - case object EnableCandidatePrecomputedNotificationFeatures - extends FSParam[Boolean]( - name = - FeatureHydrationSourcesFeatureSwitchKeys.EnableCandidatePrecomputedNotificationFeatures, - default = false - ) - - case object EnableCandidateClientFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableCandidateClientFeatures, - default = false - ) - - case object EnableUserClientFeatures - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.EnableUserClientFeatures, - default = false - ) - - case object EnableSeparateClientForTimelinesAuthors - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.UseSeparateClientForTimelinesAuthor, - default = false - ) - - case object EnableSeparateClientForMetricCenterUserCounting - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.UseSeparateClientMetricCenterUserCounting, - default = false - ) - - case object EnableSeparateClientForNotifications - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.UseSeparateClientForNotifications, - default = false - ) - - case object EnableSeparateClientForGizmoduck - extends FSParam[Boolean]( - name = FeatureHydrationSourcesFeatureSwitchKeys.UseSeparateClientForGizmoduck, - default = false - ) - - case object GlobalFetchTimeout - extends FSBoundedParam[Duration]( - name = FeatureHydrationSourcesFeatureSwitchKeys.FeatureHydrationTimeout, - default = 240.millisecond, - min = 100.millisecond, - max = 400.millisecond) - with HasDurationConversion { - override def durationConversion: DurationConversion = DurationConversion.FromMillis - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreTimelinesAuthorSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreTimelinesAuthorSource.scala deleted file mode 100644 index 179ae7081..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreTimelinesAuthorSource.scala +++ /dev/null @@ -1,191 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.github.benmanes.caffeine.cache.Caffeine -import com.google.inject.Inject -import com.twitter.finagle.TimeoutException -import com.twitter.finagle.mtls.authentication.ServiceIdentifier -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSource -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSourceId -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.feature_hydration.sources.Utils.adaptAdditionalFeaturesToDataRecord -import com.twitter.follow_recommendations.common.feature_hydration.sources.Utils.randomizedTTL -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.ml.api.IRecordOneToOneAdapter -import com.twitter.ml.featurestore.catalog.datasets.timelines.AuthorFeaturesEntityDataset -import com.twitter.ml.featurestore.catalog.entities.core.{Author => AuthorEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{AuthorTopic => AuthorTopicEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{CandidateUser => CandidateUserEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{User => UserEntity} -import com.twitter.ml.featurestore.lib.EdgeEntityId -import com.twitter.ml.featurestore.lib.EntityId -import com.twitter.ml.featurestore.lib.TopicId -import com.twitter.ml.featurestore.lib.UserId -import com.twitter.ml.featurestore.lib.data.PredictionRecord -import com.twitter.ml.featurestore.lib.data.PredictionRecordAdapter -import com.twitter.ml.featurestore.lib.dataset.DatasetId -import com.twitter.ml.featurestore.lib.dataset.online.Hydrator.HydrationResponse -import com.twitter.ml.featurestore.lib.dataset.online.OnlineAccessDataset -import com.twitter.ml.featurestore.lib.dynamic.ClientConfig -import com.twitter.ml.featurestore.lib.dynamic.DynamicFeatureStoreClient -import com.twitter.ml.featurestore.lib.dynamic.DynamicHydrationConfig -import com.twitter.ml.featurestore.lib.dynamic.FeatureStoreParamsConfig -import com.twitter.ml.featurestore.lib.dynamic.GatedFeatures -import com.twitter.ml.featurestore.lib.feature.BoundFeature -import com.twitter.ml.featurestore.lib.feature.BoundFeatureSet -import com.twitter.ml.featurestore.lib.online.DatasetValuesCache -import com.twitter.ml.featurestore.lib.online.FeatureStoreRequest -import com.twitter.ml.featurestore.lib.online.OnlineFeatureGenerationStats -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import java.util.concurrent.TimeUnit -import com.twitter.conversions.DurationOps._ -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext - -class FeatureStoreTimelinesAuthorSource @Inject() ( - serviceIdentifier: ServiceIdentifier, - stats: StatsReceiver) - extends FeatureSource { - import FeatureStoreTimelinesAuthorSource._ - - val backupSourceStats = stats.scope("feature_store_hydration_timelines_author") - val adapterStats = backupSourceStats.scope("adapters") - override def id: FeatureSourceId = FeatureSourceId.FeatureStoreTimelinesAuthorSourceId - override def featureContext: FeatureContext = getFeatureContext - - val clientConfig: ClientConfig[HasParams] = ClientConfig( - dynamicHydrationConfig = dynamicHydrationConfig, - featureStoreParamsConfig = - FeatureStoreParamsConfig(FeatureStoreParameters.featureStoreParams, Map.empty), - /** - * The smaller one between `timeoutProvider` and `FeatureStoreSourceParams.GlobalFetchTimeout` - * used below takes effect. - */ - timeoutProvider = Function.const(800.millis), - serviceIdentifier = serviceIdentifier - ) - - private val datasetsToCache = Set( - AuthorFeaturesEntityDataset - ).asInstanceOf[Set[OnlineAccessDataset[_ <: EntityId, _]]] - - private val datasetValuesCache: DatasetValuesCache = - DatasetValuesCache( - Caffeine - .newBuilder() - .expireAfterWrite(randomizedTTL(12.hours.inSeconds), TimeUnit.SECONDS) - .maximumSize(DefaultCacheMaxKeys) - .build[(_ <: EntityId, DatasetId), Stitch[HydrationResponse[_]]] - .asMap, - datasetsToCache, - DatasetCacheScope - ) - - private val dynamicFeatureStoreClient = DynamicFeatureStoreClient( - clientConfig, - backupSourceStats, - Set(datasetValuesCache) - ) - - private val adapter: IRecordOneToOneAdapter[PredictionRecord] = - PredictionRecordAdapter.oneToOne( - BoundFeatureSet(allFeatures), - OnlineFeatureGenerationStats(backupSourceStats) - ) - - override def hydrateFeatures( - target: HasClientContext - with HasPreFetchedFeature - with HasParams - with HasSimilarToContext - with HasDisplayLocation, - candidates: Seq[CandidateUser] - ): Stitch[Map[CandidateUser, DataRecord]] = { - target.getOptionalUserId - .map { targetUserId => - val featureRequests = candidates.map { candidate => - val userEntityId = UserEntity.withId(UserId(targetUserId)) - val candidateEntityId = CandidateUserEntity.withId(UserId(candidate.id)) - val similarToUserId = target.similarToUserIds.map(id => AuthorEntity.withId(UserId(id))) - val topicProof = candidate.reason.flatMap(_.accountProof.flatMap(_.topicProof)) - val authorTopicEntity = if (topicProof.isDefined) { - backupSourceStats.counter("candidates_with_topic_proof").incr() - Set( - AuthorTopicEntity.withId( - EdgeEntityId(UserId(candidate.id), TopicId(topicProof.get.topicId)))) - } else Nil - - val entities = - Seq(userEntityId, candidateEntityId) ++ similarToUserId ++ authorTopicEntity - FeatureStoreRequest(entities) - } - - val predictionRecordsFut = dynamicFeatureStoreClient(featureRequests, target) - val candidateFeatureMap = predictionRecordsFut.map { predictionRecords => - // we can zip predictionRecords with candidates as the order is preserved in the client - candidates - .zip(predictionRecords).map { - case (candidate, predictionRecord) => - candidate -> adaptAdditionalFeaturesToDataRecord( - adapter.adaptToDataRecord(predictionRecord), - adapterStats, - FeatureStoreSource.featureAdapters) - }.toMap - } - Stitch - .callFuture(candidateFeatureMap) - .within(target.params(FeatureStoreSourceParams.GlobalFetchTimeout))( - com.twitter.finagle.util.DefaultTimer) - .rescue { - case _: TimeoutException => - Stitch.value(Map.empty[CandidateUser, DataRecord]) - } - }.getOrElse(Stitch.value(Map.empty[CandidateUser, DataRecord])) - } -} - -object FeatureStoreTimelinesAuthorSource { - private val DatasetCacheScope = "feature_store_local_cache_timelines_author" - private val DefaultCacheMaxKeys = 20000 - - import FeatureStoreFeatures._ - - val allFeatures: Set[BoundFeature[_ <: EntityId, _]] = - similarToUserTimelinesAuthorAggregateFeatures ++ - candidateUserTimelinesAuthorAggregateFeatures ++ - authorTopicFeatures - - val getFeatureContext: FeatureContext = - BoundFeatureSet(allFeatures).toFeatureContext - - val dynamicHydrationConfig: DynamicHydrationConfig[HasParams] = - DynamicHydrationConfig( - Set( - GatedFeatures( - boundFeatureSet = BoundFeatureSet(authorTopicFeatures), - gate = - HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForTimelinesAuthors) & - HasParams.paramGate(FeatureStoreSourceParams.EnableAuthorTopicAggregateFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(similarToUserTimelinesAuthorAggregateFeatures), - gate = - HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForTimelinesAuthors) & - HasParams.paramGate(FeatureStoreSourceParams.EnableSimilarToUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(candidateUserTimelinesAuthorAggregateFeatures), - gate = - HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForTimelinesAuthors) & - HasParams.paramGate( - FeatureStoreSourceParams.EnableCandidateUserTimelinesAuthorAggregateFeatures) - ), - )) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreUserMetricCountsSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreUserMetricCountsSource.scala deleted file mode 100644 index 110985c92..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/FeatureStoreUserMetricCountsSource.scala +++ /dev/null @@ -1,187 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.github.benmanes.caffeine.cache.Caffeine -import com.google.inject.Inject -import com.twitter.finagle.TimeoutException -import com.twitter.finagle.mtls.authentication.ServiceIdentifier -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSource -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSourceId -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.feature_hydration.sources.Utils.adaptAdditionalFeaturesToDataRecord -import com.twitter.follow_recommendations.common.feature_hydration.sources.Utils.randomizedTTL -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.ml.api.IRecordOneToOneAdapter -import com.twitter.ml.featurestore.catalog.datasets.onboarding.MetricCenterUserCountingFeaturesDataset -import com.twitter.ml.featurestore.catalog.entities.core.{Author => AuthorEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{AuthorTopic => AuthorTopicEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{CandidateUser => CandidateUserEntity} -import com.twitter.ml.featurestore.catalog.entities.core.{User => UserEntity} -import com.twitter.ml.featurestore.lib.EdgeEntityId -import com.twitter.ml.featurestore.lib.EntityId -import com.twitter.ml.featurestore.lib.TopicId -import com.twitter.ml.featurestore.lib.UserId -import com.twitter.ml.featurestore.lib.data.PredictionRecord -import com.twitter.ml.featurestore.lib.data.PredictionRecordAdapter -import com.twitter.ml.featurestore.lib.dataset.DatasetId -import com.twitter.ml.featurestore.lib.dataset.online.Hydrator.HydrationResponse -import com.twitter.ml.featurestore.lib.dataset.online.OnlineAccessDataset -import com.twitter.ml.featurestore.lib.dynamic.ClientConfig -import com.twitter.ml.featurestore.lib.dynamic.DynamicFeatureStoreClient -import com.twitter.ml.featurestore.lib.dynamic.DynamicHydrationConfig -import com.twitter.ml.featurestore.lib.dynamic.FeatureStoreParamsConfig -import com.twitter.ml.featurestore.lib.dynamic.GatedFeatures -import com.twitter.ml.featurestore.lib.feature.BoundFeature -import com.twitter.ml.featurestore.lib.feature.BoundFeatureSet -import com.twitter.ml.featurestore.lib.online.DatasetValuesCache -import com.twitter.ml.featurestore.lib.online.FeatureStoreRequest -import com.twitter.ml.featurestore.lib.online.OnlineFeatureGenerationStats -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import java.util.concurrent.TimeUnit -import com.twitter.conversions.DurationOps._ -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext - -class FeatureStoreUserMetricCountsSource @Inject() ( - serviceIdentifier: ServiceIdentifier, - stats: StatsReceiver) - extends FeatureSource { - import FeatureStoreUserMetricCountsSource._ - - val backupSourceStats = stats.scope("feature_store_hydration_mc_counting") - val adapterStats = backupSourceStats.scope("adapters") - override def id: FeatureSourceId = FeatureSourceId.FeatureStoreUserMetricCountsSourceId - override def featureContext: FeatureContext = getFeatureContext - - val clientConfig: ClientConfig[HasParams] = ClientConfig( - dynamicHydrationConfig = dynamicHydrationConfig, - featureStoreParamsConfig = - FeatureStoreParamsConfig(FeatureStoreParameters.featureStoreParams, Map.empty), - /** - * The smaller one between `timeoutProvider` and `FeatureStoreSourceParams.GlobalFetchTimeout` - * used below takes effect. - */ - timeoutProvider = Function.const(800.millis), - serviceIdentifier = serviceIdentifier - ) - - private val datasetsToCache = Set( - MetricCenterUserCountingFeaturesDataset - ).asInstanceOf[Set[OnlineAccessDataset[_ <: EntityId, _]]] - - private val datasetValuesCache: DatasetValuesCache = - DatasetValuesCache( - Caffeine - .newBuilder() - .expireAfterWrite(randomizedTTL(12.hours.inSeconds), TimeUnit.SECONDS) - .maximumSize(DefaultCacheMaxKeys) - .build[(_ <: EntityId, DatasetId), Stitch[HydrationResponse[_]]] - .asMap, - datasetsToCache, - DatasetCacheScope - ) - - private val dynamicFeatureStoreClient = DynamicFeatureStoreClient( - clientConfig, - backupSourceStats, - Set(datasetValuesCache) - ) - - private val adapter: IRecordOneToOneAdapter[PredictionRecord] = - PredictionRecordAdapter.oneToOne( - BoundFeatureSet(allFeatures), - OnlineFeatureGenerationStats(backupSourceStats) - ) - - override def hydrateFeatures( - target: HasClientContext - with HasPreFetchedFeature - with HasParams - with HasSimilarToContext - with HasDisplayLocation, - candidates: Seq[CandidateUser] - ): Stitch[Map[CandidateUser, DataRecord]] = { - target.getOptionalUserId - .map { targetUserId => - val featureRequests = candidates.map { candidate => - val userEntityId = UserEntity.withId(UserId(targetUserId)) - val candidateEntityId = CandidateUserEntity.withId(UserId(candidate.id)) - val similarToUserId = target.similarToUserIds.map(id => AuthorEntity.withId(UserId(id))) - val topicProof = candidate.reason.flatMap(_.accountProof.flatMap(_.topicProof)) - val authorTopicEntity = if (topicProof.isDefined) { - backupSourceStats.counter("candidates_with_topic_proof").incr() - Set( - AuthorTopicEntity.withId( - EdgeEntityId(UserId(candidate.id), TopicId(topicProof.get.topicId)))) - } else Nil - - val entities = - Seq(userEntityId, candidateEntityId) ++ similarToUserId ++ authorTopicEntity - FeatureStoreRequest(entities) - } - - val predictionRecordsFut = dynamicFeatureStoreClient(featureRequests, target) - val candidateFeatureMap = predictionRecordsFut.map { predictionRecords => - // we can zip predictionRecords with candidates as the order is preserved in the client - candidates - .zip(predictionRecords).map { - case (candidate, predictionRecord) => - candidate -> adaptAdditionalFeaturesToDataRecord( - adapter.adaptToDataRecord(predictionRecord), - adapterStats, - FeatureStoreSource.featureAdapters) - }.toMap - } - Stitch - .callFuture(candidateFeatureMap) - .within(target.params(FeatureStoreSourceParams.GlobalFetchTimeout))( - com.twitter.finagle.util.DefaultTimer) - .rescue { - case _: TimeoutException => - Stitch.value(Map.empty[CandidateUser, DataRecord]) - } - }.getOrElse(Stitch.value(Map.empty[CandidateUser, DataRecord])) - } -} - -object FeatureStoreUserMetricCountsSource { - private val DatasetCacheScope = "feature_store_local_cache_mc_user_counting" - private val DefaultCacheMaxKeys = 20000 - - val allFeatures: Set[BoundFeature[_ <: EntityId, _]] = - FeatureStoreFeatures.candidateUserMetricCountFeatures ++ - FeatureStoreFeatures.similarToUserMetricCountFeatures ++ - FeatureStoreFeatures.targetUserMetricCountFeatures - - val getFeatureContext: FeatureContext = - BoundFeatureSet(allFeatures).toFeatureContext - - val dynamicHydrationConfig: DynamicHydrationConfig[HasParams] = - DynamicHydrationConfig( - Set( - GatedFeatures( - boundFeatureSet = BoundFeatureSet(FeatureStoreFeatures.targetUserMetricCountFeatures), - gate = HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForMetricCenterUserCounting) & - HasParams.paramGate(FeatureStoreSourceParams.EnableTargetUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(FeatureStoreFeatures.candidateUserMetricCountFeatures), - gate = - HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForMetricCenterUserCounting) & - HasParams.paramGate(FeatureStoreSourceParams.EnableCandidateUserFeatures) - ), - GatedFeatures( - boundFeatureSet = BoundFeatureSet(FeatureStoreFeatures.similarToUserMetricCountFeatures), - gate = - HasParams - .paramGate(FeatureStoreSourceParams.EnableSeparateClientForMetricCenterUserCounting) & - HasParams.paramGate(FeatureStoreSourceParams.EnableSimilarToUserFeatures) - ), - )) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/HydrationSourcesModule.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/HydrationSourcesModule.scala deleted file mode 100644 index 59e3ea186..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/HydrationSourcesModule.scala +++ /dev/null @@ -1,152 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.google.inject.Provides -import com.google.inject.Singleton -import com.twitter.escherbird.util.stitchcache.StitchCache -import com.twitter.finagle.mtls.authentication.ServiceIdentifier -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.inject.TwitterModule -import com.twitter.stitch.Stitch -import com.twitter.storage.client.manhattan.bijections.Bijections.BinaryCompactScalaInjection -import com.twitter.storage.client.manhattan.bijections.Bijections.LongInjection -import com.twitter.storage.client.manhattan.kv.Guarantee -import com.twitter.storage.client.manhattan.kv.ManhattanKVClient -import com.twitter.storage.client.manhattan.kv.ManhattanKVClientMtlsParams -import com.twitter.storage.client.manhattan.kv.ManhattanKVEndpoint -import com.twitter.storage.client.manhattan.kv.ManhattanKVEndpointBuilder -import com.twitter.storage.client.manhattan.kv.impl.Component -import com.twitter.storage.client.manhattan.kv.impl.Component0 -import com.twitter.storage.client.manhattan.kv.impl.KeyDescriptor -import com.twitter.storage.client.manhattan.kv.impl.ValueDescriptor -import com.twitter.strato.generated.client.ml.featureStore.McUserCountingOnUserClientColumn -import com.twitter.strato.generated.client.ml.featureStore.onboarding.TimelinesAuthorFeaturesOnUserClientColumn -import com.twitter.timelines.author_features.v1.thriftscala.AuthorFeatures -import com.twitter.conversions.DurationOps._ -import com.twitter.onboarding.relevance.features.thriftscala.MCUserCountingFeatures -import java.lang.{Long => JLong} -import scala.util.Random - -object HydrationSourcesModule extends TwitterModule { - - val readFromManhattan = flag( - "feature_hydration_enable_reading_from_manhattan", - false, - "Whether to read the data from Manhattan or Strato") - - val manhattanAppId = - flag("frs_readonly.appId", "ml_features_athena", "RO App Id used by the RO FRS service") - val manhattanDestName = flag( - "frs_readonly.destName", - "/s/manhattan/athena.native-thrift", - "manhattan Dest Name used by the RO FRS service") - - @Provides - @Singleton - def providesAthenaManhattanClient( - serviceIdentifier: ServiceIdentifier - ): ManhattanKVEndpoint = { - val client = ManhattanKVClient( - manhattanAppId(), - manhattanDestName(), - ManhattanKVClientMtlsParams(serviceIdentifier) - ) - ManhattanKVEndpointBuilder(client) - .defaultGuarantee(Guarantee.Weak) - .build() - } - - val manhattanAuthorDataset = "timelines_author_features" - private val defaultCacheMaxKeys = 60000 - private val cacheTTL = 12.hours - private val earlyExpiration = 0.2 - - val authorKeyDesc = KeyDescriptor(Component(LongInjection), Component0) - val authorDatasetKey = authorKeyDesc.withDataset(manhattanAuthorDataset) - val authorValDesc = ValueDescriptor(BinaryCompactScalaInjection(AuthorFeatures)) - - @Provides - @Singleton - def timelinesAuthorStitchCache( - manhattanReadOnlyEndpoint: ManhattanKVEndpoint, - timelinesAuthorFeaturesColumn: TimelinesAuthorFeaturesOnUserClientColumn, - stats: StatsReceiver - ): StitchCache[JLong, Option[AuthorFeatures]] = { - - val stitchCacheStats = - stats - .scope("direct_ds_source_feature_hydration_module").scope("timelines_author") - - val stStat = stitchCacheStats.counter("readFromStrato-each") - val mhtStat = stitchCacheStats.counter("readFromManhattan-each") - - val timelinesAuthorUnderlyingCall = if (readFromManhattan()) { - stitchCacheStats.counter("readFromManhattan").incr() - val authorCacheUnderlyingManhattanCall: JLong => Stitch[Option[AuthorFeatures]] = id => { - mhtStat.incr() - val key = authorDatasetKey.withPkey(id) - manhattanReadOnlyEndpoint - .get(key = key, valueDesc = authorValDesc).map(_.map(value => - clearUnsedFieldsForAuthorFeature(value.contents))) - } - authorCacheUnderlyingManhattanCall - } else { - stitchCacheStats.counter("readFromStrato").incr() - val authorCacheUnderlyingStratoCall: JLong => Stitch[Option[AuthorFeatures]] = id => { - stStat.incr() - val timelinesAuthorFeaturesFetcher = timelinesAuthorFeaturesColumn.fetcher - timelinesAuthorFeaturesFetcher - .fetch(id).map(result => result.v.map(clearUnsedFieldsForAuthorFeature)) - } - authorCacheUnderlyingStratoCall - } - - StitchCache[JLong, Option[AuthorFeatures]]( - underlyingCall = timelinesAuthorUnderlyingCall, - maxCacheSize = defaultCacheMaxKeys, - ttl = randomizedTTL(cacheTTL.inSeconds).seconds, - statsReceiver = stitchCacheStats - ) - - } - - // Not adding manhattan since it didn't seem useful for Author Data, we can add in another phab - // if deemed helpful - @Provides - @Singleton - def metricCenterUserCountingStitchCache( - mcUserCountingFeaturesColumn: McUserCountingOnUserClientColumn, - stats: StatsReceiver - ): StitchCache[JLong, Option[MCUserCountingFeatures]] = { - - val stitchCacheStats = - stats - .scope("direct_ds_source_feature_hydration_module").scope("mc_user_counting") - - val stStat = stitchCacheStats.counter("readFromStrato-each") - stitchCacheStats.counter("readFromStrato").incr() - - val mcUserCountingCacheUnderlyingCall: JLong => Stitch[Option[MCUserCountingFeatures]] = id => { - stStat.incr() - val mcUserCountingFeaturesFetcher = mcUserCountingFeaturesColumn.fetcher - mcUserCountingFeaturesFetcher.fetch(id).map(_.v) - } - - StitchCache[JLong, Option[MCUserCountingFeatures]]( - underlyingCall = mcUserCountingCacheUnderlyingCall, - maxCacheSize = defaultCacheMaxKeys, - ttl = randomizedTTL(cacheTTL.inSeconds).seconds, - statsReceiver = stitchCacheStats - ) - - } - - // clear out fields we don't need to save cache space - private def clearUnsedFieldsForAuthorFeature(entry: AuthorFeatures): AuthorFeatures = { - entry.unsetUserTopics.unsetUserHealth.unsetAuthorCountryCodeAggregates.unsetOriginalAuthorCountryCodeAggregates - } - - // To avoid a cache stampede. See https://en.wikipedia.org/wiki/Cache_stampede - private def randomizedTTL(ttl: Long): Long = { - (ttl - ttl * earlyExpiration * Random.nextDouble()).toLong - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/PreFetchedFeatureSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/PreFetchedFeatureSource.scala deleted file mode 100644 index 51975c487..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/PreFetchedFeatureSource.scala +++ /dev/null @@ -1,36 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.google.inject.Inject -import com.google.inject.Provides -import com.google.inject.Singleton -import com.twitter.follow_recommendations.common.feature_hydration.adapters.PreFetchedFeatureAdapter -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSource -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSourceId -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -@Provides -@Singleton -class PreFetchedFeatureSource @Inject() () extends FeatureSource { - override def id: FeatureSourceId = FeatureSourceId.PreFetchedFeatureSourceId - override def featureContext: FeatureContext = PreFetchedFeatureAdapter.getFeatureContext - override def hydrateFeatures( - target: HasClientContext - with HasPreFetchedFeature - with HasParams - with HasSimilarToContext - with HasDisplayLocation, - candidates: Seq[CandidateUser] - ): Stitch[Map[CandidateUser, DataRecord]] = { - Stitch.value(candidates.map { candidate => - candidate -> PreFetchedFeatureAdapter.adaptToDataRecord((target, candidate)) - }.toMap) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/UserScoringFeatureSource.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/UserScoringFeatureSource.scala deleted file mode 100644 index 155d9e442..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/UserScoringFeatureSource.scala +++ /dev/null @@ -1,86 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.google.inject.Inject -import com.google.inject.Provides -import com.google.inject.Singleton -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSource -import com.twitter.follow_recommendations.common.feature_hydration.common.FeatureSourceId -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.DataRecordMerger -import com.twitter.ml.api.FeatureContext -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -/** - * This source wraps around the separate sources that we hydrate features from - * @param featureStoreSource gets features that require a RPC call to feature store - * @param stratoFeatureHydrationSource gets features that require a RPC call to strato columns - * @param clientContextSource gets features that are already present in the request context - * @param candidateAlgorithmSource gets features that are already present from candidate generation - * @param preFetchedFeatureSource gets features that were prehydrated (shared in request lifecycle) - */ -@Provides -@Singleton -class UserScoringFeatureSource @Inject() ( - featureStoreSource: FeatureStoreSource, - featureStoreGizmoduckSource: FeatureStoreGizmoduckSource, - featureStorePostNuxAlgorithmSource: FeatureStorePostNuxAlgorithmSource, - featureStoreTimelinesAuthorSource: FeatureStoreTimelinesAuthorSource, - featureStoreUserMetricCountsSource: FeatureStoreUserMetricCountsSource, - clientContextSource: ClientContextSource, - candidateAlgorithmSource: CandidateAlgorithmSource, - preFetchedFeatureSource: PreFetchedFeatureSource) - extends FeatureSource { - - override val id: FeatureSourceId = FeatureSourceId.UserScoringFeatureSourceId - - override val featureContext: FeatureContext = FeatureContext.merge( - featureStoreSource.featureContext, - featureStoreGizmoduckSource.featureContext, - featureStorePostNuxAlgorithmSource.featureContext, - featureStoreTimelinesAuthorSource.featureContext, - featureStoreUserMetricCountsSource.featureContext, - clientContextSource.featureContext, - candidateAlgorithmSource.featureContext, - preFetchedFeatureSource.featureContext, - ) - - val sources = - Seq( - featureStoreSource, - featureStorePostNuxAlgorithmSource, - featureStoreTimelinesAuthorSource, - featureStoreUserMetricCountsSource, - featureStoreGizmoduckSource, - clientContextSource, - candidateAlgorithmSource, - preFetchedFeatureSource - ) - - val dataRecordMerger = new DataRecordMerger - - def hydrateFeatures( - target: HasClientContext - with HasPreFetchedFeature - with HasParams - with HasSimilarToContext - with HasDisplayLocation, - candidates: Seq[CandidateUser] - ): Stitch[Map[CandidateUser, DataRecord]] = { - Stitch.collect(sources.map(_.hydrateFeatures(target, candidates))).map { featureMaps => - (for { - candidate <- candidates - } yield { - val combinedDataRecord = new DataRecord - featureMaps - .flatMap(_.get(candidate).toSeq).foreach(dataRecordMerger.merge(combinedDataRecord, _)) - candidate -> combinedDataRecord - }).toMap - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/Utils.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/Utils.scala deleted file mode 100644 index 99bd71310..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources/Utils.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.twitter.follow_recommendations.common.feature_hydration.sources - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.IRecordOneToOneAdapter -import scala.util.Random - -/** - * Helper functions for FeatureStoreSource operations in FRS are available here. - */ -object Utils { - - private val EarlyExpiration = 0.2 - - private[common] def adaptAdditionalFeaturesToDataRecord( - record: DataRecord, - adapterStats: StatsReceiver, - featureAdapters: Seq[IRecordOneToOneAdapter[DataRecord]] - ): DataRecord = { - featureAdapters.foldRight(record) { (adapter, record) => - adapterStats.counter(adapter.getClass.getSimpleName).incr() - adapter.adaptToDataRecord(record) - } - } - - // To avoid a cache stampede. See https://en.wikipedia.org/wiki/Cache_stampede - private[common] def randomizedTTL(ttl: Long): Long = { - (ttl - ttl * EarlyExpiration * Random.nextDouble()).toLong - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/BUILD deleted file mode 100644 index b5ece498a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/LocationFeature.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/LocationFeature.scala deleted file mode 100644 index 5325c6b56..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/LocationFeature.scala +++ /dev/null @@ -1,10 +0,0 @@ -package com.twitter.follow_recommendations.common.features - -import com.twitter.follow_recommendations.common.models.GeohashAndCountryCode -import com.twitter.product_mixer.core.feature.FeatureWithDefaultOnFailure -import com.twitter.product_mixer.core.pipeline.PipelineQuery - -case object LocationFeature - extends FeatureWithDefaultOnFailure[PipelineQuery, Option[GeohashAndCountryCode]] { - override val defaultValue: Option[GeohashAndCountryCode] = None -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/TrackingTokenFeature.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/TrackingTokenFeature.scala deleted file mode 100644 index 23571c8df..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/TrackingTokenFeature.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.twitter.follow_recommendations.common.features - -import com.twitter.product_mixer.core.feature.FeatureWithDefaultOnFailure -import com.twitter.product_mixer.core.pipeline.PipelineQuery - -case object TrackingTokenFeature extends FeatureWithDefaultOnFailure[PipelineQuery, Option[Int]] { - override val defaultValue: Option[Int] = None -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/UserStateFeature.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/UserStateFeature.scala deleted file mode 100644 index 73072b295..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features/UserStateFeature.scala +++ /dev/null @@ -1,7 +0,0 @@ -package com.twitter.follow_recommendations.common.features - -import com.twitter.core_workflows.user_model.thriftscala.UserState -import com.twitter.product_mixer.core.feature.Feature -import com.twitter.product_mixer.core.pipeline.PipelineQuery - -case object UserStateFeature extends Feature[PipelineQuery, Option[UserState]] {} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/AddressBookMetadata.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/AddressBookMetadata.scala deleted file mode 100644 index 303417d23..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/AddressBookMetadata.scala +++ /dev/null @@ -1,29 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier - -/** - * contains information if a candidate is from a candidate source generated using the following signals. - */ -case class AddressBookMetadata( - inForwardPhoneBook: Boolean, - inReversePhoneBook: Boolean, - inForwardEmailBook: Boolean, - inReverseEmailBook: Boolean) - -object AddressBookMetadata { - - val ForwardPhoneBookCandidateSource = CandidateSourceIdentifier( - Algorithm.ForwardPhoneBook.toString) - - val ReversePhoneBookCandidateSource = CandidateSourceIdentifier( - Algorithm.ReversePhoneBook.toString) - - val ForwardEmailBookCandidateSource = CandidateSourceIdentifier( - Algorithm.ForwardEmailBook.toString) - - val ReverseEmailBookCandidateSource = CandidateSourceIdentifier( - Algorithm.ReverseEmailBookIbis.toString) - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/AlgorithmType.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/AlgorithmType.scala deleted file mode 100644 index b60afb8b3..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/AlgorithmType.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -/** - * Each candidate source algorithm could be based on one, or more, of the 4 general type of - * information we have on a user: - * 1. Social: the user's connections in Twitter's social graph. - * 2. Geo: the user's geographical information. - * 3. Interest: information on the user's chosen interests. - * 4. Activity: information on the user's past activity. - * - * Note that an algorithm can fall under more than one of these categories. - */ -object AlgorithmType extends Enumeration { - type AlgorithmType = Value - - val Social: Value = Value("social") - val Geo: Value = Value("geo") - val Activity: Value = Value("activity") - val Interest: Value = Value("interest") -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/BUILD deleted file mode 100644 index c4916b6d0..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/BUILD +++ /dev/null @@ -1,29 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common", - "follow-recommendations-service/thrift/src/main/thrift:thrift-scala", - "hermit/hermit-core/src/main/scala/com/twitter/hermit/constants", - "hermit/hermit-core/src/main/scala/com/twitter/hermit/model", - "hermit/hermit-ml/src/main/scala/com/twitter/hermit/ml/models", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/candidate_source", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common/identifier", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request", - "product-mixer/core/src/main/thrift/com/twitter/product_mixer/core:thrift-scala", - "scrooge/scrooge-serializer/src/main/scala", - "src/java/com/twitter/ml/api:api-base", - "src/scala/com/twitter/ml/api/util", - "src/scala/com/twitter/wtf/scalding/jobs/strong_tie_prediction", - "src/thrift/com/twitter/ads/adserver:adserver_rpc-scala", - "src/thrift/com/twitter/timelines/author_features/user_health:thrift-scala", - "user-signal-service/thrift/src/main/thrift:thrift-scala", - "util/util-slf4j-api/src/main/scala/com/twitter/util/logging", - ], - exports = [ - "util/util-slf4j-api/src/main/scala/com/twitter/util/logging", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/CandidateUser.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/CandidateUser.scala deleted file mode 100644 index 178f34b30..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/CandidateUser.scala +++ /dev/null @@ -1,192 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.{thriftscala => t} -import com.twitter.hermit.constants.AlgorithmFeedbackTokens -import com.twitter.ml.api.thriftscala.{DataRecord => TDataRecord} -import com.twitter.ml.api.util.ScalaToJavaDataRecordConversions -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Params -import com.twitter.product_mixer.core.model.common.UniversalNoun -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier - -trait FollowableEntity extends UniversalNoun[Long] - -trait Recommendation - extends FollowableEntity - with HasReason - with HasAdMetadata - with HasTrackingToken { - val score: Option[Double] - - def toThrift: t.Recommendation - - def toOfflineThrift: offline.OfflineRecommendation -} - -case class CandidateUser( - override val id: Long, - override val score: Option[Double] = None, - override val reason: Option[Reason] = None, - override val userCandidateSourceDetails: Option[UserCandidateSourceDetails] = None, - override val adMetadata: Option[AdMetadata] = None, - override val trackingToken: Option[TrackingToken] = None, - override val dataRecord: Option[RichDataRecord] = None, - override val scores: Option[Scores] = None, - override val infoPerRankingStage: Option[scala.collection.Map[String, RankingInfo]] = None, - override val params: Params = Params.Invalid, - override val engagements: Seq[EngagementType] = Nil, - override val recommendationFlowIdentifier: Option[String] = None) - extends Recommendation - with HasUserCandidateSourceDetails - with HasDataRecord - with HasScores - with HasParams - with HasEngagements - with HasRecommendationFlowIdentifier - with HasInfoPerRankingStage { - - val rankerIdsStr: Option[Seq[String]] = { - val strs = scores.map(_.scores.flatMap(_.rankerId.map(_.toString))) - if (strs.exists(_.nonEmpty)) strs else None - } - - val thriftDataRecord: Option[TDataRecord] = for { - richDataRecord <- dataRecord - dr <- richDataRecord.dataRecord - } yield { - ScalaToJavaDataRecordConversions.javaDataRecord2ScalaDataRecord(dr) - } - - val toOfflineUserThrift: offline.OfflineUserRecommendation = { - val scoringDetails = - if (userCandidateSourceDetails.isEmpty && score.isEmpty && thriftDataRecord.isEmpty) { - None - } else { - Some( - offline.ScoringDetails( - candidateSourceDetails = userCandidateSourceDetails.map(_.toOfflineThrift), - score = score, - dataRecord = thriftDataRecord, - rankerIds = rankerIdsStr, - infoPerRankingStage = infoPerRankingStage.map(_.mapValues(_.toOfflineThrift)) - ) - ) - } - offline - .OfflineUserRecommendation( - id, - reason.map(_.toOfflineThrift), - adMetadata.map(_.adImpression), - trackingToken.map(_.toOfflineThrift), - scoringDetails = scoringDetails - ) - } - - override val toOfflineThrift: offline.OfflineRecommendation = - offline.OfflineRecommendation.User(toOfflineUserThrift) - - val toUserThrift: t.UserRecommendation = { - val scoringDetails = - if (userCandidateSourceDetails.isEmpty && score.isEmpty && thriftDataRecord.isEmpty && scores.isEmpty) { - None - } else { - Some( - t.ScoringDetails( - candidateSourceDetails = userCandidateSourceDetails.map(_.toThrift), - score = score, - dataRecord = thriftDataRecord, - rankerIds = rankerIdsStr, - debugDataRecord = dataRecord.flatMap(_.debugDataRecord), - infoPerRankingStage = infoPerRankingStage.map(_.mapValues(_.toThrift)) - ) - ) - } - t.UserRecommendation( - userId = id, - reason = reason.map(_.toThrift), - adImpression = adMetadata.map(_.adImpression), - trackingInfo = trackingToken.map(TrackingToken.serialize), - scoringDetails = scoringDetails, - recommendationFlowIdentifier = recommendationFlowIdentifier - ) - } - - override val toThrift: t.Recommendation = - t.Recommendation.User(toUserThrift) - - def setFollowProof(followProofOpt: Option[FollowProof]): CandidateUser = { - this.copy( - reason = reason - .map { reason => - reason.copy( - accountProof = reason.accountProof - .map { accountProof => - accountProof.copy(followProof = followProofOpt) - }.orElse(Some(AccountProof(followProof = followProofOpt))) - ) - }.orElse(Some(Reason(Some(AccountProof(followProof = followProofOpt))))) - ) - } - - def addScore(score: Score): CandidateUser = { - val newScores = scores match { - case Some(existingScores) => existingScores.copy(scores = existingScores.scores :+ score) - case None => Scores(Seq(score)) - } - this.copy(scores = Some(newScores)) - } -} - -object CandidateUser { - val DefaultCandidateScore = 1.0 - - // for converting candidate in ScoringUserRequest - def fromUserRecommendation(candidate: t.UserRecommendation): CandidateUser = { - // we only use the primary candidate source for now - val userCandidateSourceDetails = for { - scoringDetails <- candidate.scoringDetails - candidateSourceDetails <- scoringDetails.candidateSourceDetails - } yield UserCandidateSourceDetails( - primaryCandidateSource = candidateSourceDetails.primarySource - .flatMap(AlgorithmFeedbackTokens.TokenToAlgorithmMap.get).map { algo => - CandidateSourceIdentifier(algo.toString) - }, - candidateSourceScores = fromThriftScoreMap(candidateSourceDetails.candidateSourceScores), - candidateSourceRanks = fromThriftRankMap(candidateSourceDetails.candidateSourceRanks), - addressBookMetadata = None - ) - CandidateUser( - id = candidate.userId, - score = candidate.scoringDetails.flatMap(_.score), - reason = candidate.reason.map(Reason.fromThrift), - userCandidateSourceDetails = userCandidateSourceDetails, - trackingToken = candidate.trackingInfo.map(TrackingToken.deserialize), - recommendationFlowIdentifier = candidate.recommendationFlowIdentifier, - infoPerRankingStage = candidate.scoringDetails.flatMap( - _.infoPerRankingStage.map(_.mapValues(RankingInfo.fromThrift))) - ) - } - - def fromThriftScoreMap( - thriftMapOpt: Option[scala.collection.Map[String, Double]] - ): Map[CandidateSourceIdentifier, Option[Double]] = { - (for { - thriftMap <- thriftMapOpt.toSeq - (algoName, score) <- thriftMap.toSeq - } yield { - CandidateSourceIdentifier(algoName) -> Some(score) - }).toMap - } - - def fromThriftRankMap( - thriftMapOpt: Option[scala.collection.Map[String, Int]] - ): Map[CandidateSourceIdentifier, Int] = { - (for { - thriftMap <- thriftMapOpt.toSeq - (algoName, rank) <- thriftMap.toSeq - } yield { - CandidateSourceIdentifier(algoName) -> rank - }).toMap - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/ClientContextConverter.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/ClientContextConverter.scala deleted file mode 100644 index ac601371c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/ClientContextConverter.scala +++ /dev/null @@ -1,53 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.{thriftscala => frs} -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext - -object ClientContextConverter { - def toFRSOfflineClientContextThrift( - productMixerClientContext: ClientContext - ): offline.OfflineClientContext = - offline.OfflineClientContext( - productMixerClientContext.userId, - productMixerClientContext.guestId, - productMixerClientContext.appId, - productMixerClientContext.countryCode, - productMixerClientContext.languageCode, - productMixerClientContext.guestIdAds, - productMixerClientContext.guestIdMarketing - ) - - def fromThrift(clientContext: frs.ClientContext): ClientContext = ClientContext( - userId = clientContext.userId, - guestId = clientContext.guestId, - appId = clientContext.appId, - ipAddress = clientContext.ipAddress, - userAgent = clientContext.userAgent, - countryCode = clientContext.countryCode, - languageCode = clientContext.languageCode, - isTwoffice = clientContext.isTwoffice, - userRoles = clientContext.userRoles.map(_.toSet), - deviceId = clientContext.deviceId, - guestIdAds = clientContext.guestIdAds, - guestIdMarketing = clientContext.guestIdMarketing, - mobileDeviceId = None, - mobileDeviceAdId = None, - limitAdTracking = None - ) - - def toThrift(clientContext: ClientContext): frs.ClientContext = frs.ClientContext( - userId = clientContext.userId, - guestId = clientContext.guestIdAds, - appId = clientContext.appId, - ipAddress = clientContext.ipAddress, - userAgent = clientContext.userAgent, - countryCode = clientContext.countryCode, - languageCode = clientContext.languageCode, - isTwoffice = clientContext.isTwoffice, - userRoles = clientContext.userRoles, - deviceId = clientContext.deviceId, - guestIdAds = clientContext.guestIdAds, - guestIdMarketing = clientContext.guestIdMarketing - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/DisplayLocation.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/DisplayLocation.scala deleted file mode 100644 index b49baf034..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/DisplayLocation.scala +++ /dev/null @@ -1,420 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.adserver.thriftscala.{DisplayLocation => AdDisplayLocation} -import com.twitter.follow_recommendations.logging.thriftscala.{ - OfflineDisplayLocation => TOfflineDisplayLocation -} -import com.twitter.follow_recommendations.thriftscala.{DisplayLocation => TDisplayLocation} - -sealed trait DisplayLocation { - def toThrift: TDisplayLocation - - def toOfflineThrift: TOfflineDisplayLocation - - def toFsName: String - - // corresponding display location in adserver if available - // make sure to be consistent with the definition here - def toAdDisplayLocation: Option[AdDisplayLocation] = None -} - -/** - * Make sure you add the new DL to the following files and redeploy our attribution jobs - * - follow-recommendations-service/thrift/src/main/thrift/display_location.thrift - * - follow-recommendations-service/thrift/src/main/thrift/logging/display_location.thrift - * - follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/DisplayLocation.scala - */ - -object DisplayLocation { - - case object ProfileSidebar extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.ProfileSidebar - override val toOfflineThrift: TOfflineDisplayLocation = TOfflineDisplayLocation.ProfileSidebar - override val toFsName: String = "ProfileSidebar" - - override val toAdDisplayLocation: Option[AdDisplayLocation] = Some( - AdDisplayLocation.ProfileAccountsSidebar - ) - } - - case object HomeTimeline extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.HomeTimeline - override val toOfflineThrift: TOfflineDisplayLocation = TOfflineDisplayLocation.HomeTimeline - override val toFsName: String = "HomeTimeline" - override val toAdDisplayLocation: Option[AdDisplayLocation] = Some( - // it is based on the logic that HTL DL should correspond to Sidebar: - AdDisplayLocation.WtfSidebar - ) - } - - case object ReactiveFollow extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.ReactiveFollow - override val toOfflineThrift: TOfflineDisplayLocation = TOfflineDisplayLocation.ReactiveFollow - override val toFsName: String = "ReactiveFollow" - } - - case object ExploreTab extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.ExploreTab - override val toOfflineThrift: TOfflineDisplayLocation = TOfflineDisplayLocation.ExploreTab - override val toFsName: String = "ExploreTab" - } - - case object MagicRecs extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.MagicRecs - override val toOfflineThrift: TOfflineDisplayLocation = TOfflineDisplayLocation.MagicRecs - override val toFsName: String = "MagicRecs" - } - - case object AbUploadInjection extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.AbUploadInjection - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.AbUploadInjection - override val toFsName: String = "AbUploadInjection" - } - - case object RuxLandingPage extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.RuxLandingPage - override val toOfflineThrift: TOfflineDisplayLocation = TOfflineDisplayLocation.RuxLandingPage - override val toFsName: String = "RuxLandingPage" - } - - case object ProfileBonusFollow extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.ProfileBonusFollow - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.ProfileBonusFollow - override val toFsName: String = "ProfileBonusFollow" - } - - case object ElectionExploreWtf extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.ElectionExploreWtf - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.ElectionExploreWtf - override val toFsName: String = "ElectionExploreWtf" - } - - case object ClusterFollow extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.ClusterFollow - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.ClusterFollow - override val toFsName: String = "ClusterFollow" - override val toAdDisplayLocation: Option[AdDisplayLocation] = Some( - AdDisplayLocation.ClusterFollow - ) - } - - case object HtlBonusFollow extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.HtlBonusFollow - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.HtlBonusFollow - override val toFsName: String = "HtlBonusFollow" - } - - case object TopicLandingPageHeader extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.TopicLandingPageHeader - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.TopicLandingPageHeader - override val toFsName: String = "TopicLandingPageHeader" - } - - case object NewUserSarusBackfill extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.NewUserSarusBackfill - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.NewUserSarusBackfill - override val toFsName: String = "NewUserSarusBackfill" - } - - case object NuxPymk extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.NuxPymk - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.NuxPymk - override val toFsName: String = "NuxPymk" - } - - case object NuxInterests extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.NuxInterests - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.NuxInterests - override val toFsName: String = "NuxInterests" - } - - case object NuxTopicBonusFollow extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.NuxTopicBonusFollow - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.NuxTopicBonusFollow - override val toFsName: String = "NuxTopicBonusFollow" - } - - case object Sidebar extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.Sidebar - override val toOfflineThrift: TOfflineDisplayLocation = TOfflineDisplayLocation.Sidebar - override val toFsName: String = "Sidebar" - - override val toAdDisplayLocation: Option[AdDisplayLocation] = Some( - AdDisplayLocation.WtfSidebar - ) - } - - case object CampaignForm extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.CampaignForm - override val toOfflineThrift: TOfflineDisplayLocation = TOfflineDisplayLocation.CampaignForm - override val toFsName: String = "CampaignForm" - } - - case object ProfileTopFollowers extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.ProfileTopFollowers - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.ProfileTopFollowers - override val toFsName: String = "ProfileTopFollowers" - } - - case object ProfileTopFollowing extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.ProfileTopFollowing - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.ProfileTopFollowing - override val toFsName: String = "ProfileTopFollowing" - } - - case object RuxPymk extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.RuxPymk - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.RuxPymk - override val toFsName: String = "RuxPymk" - } - - case object IndiaCovid19CuratedAccountsWtf extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.IndiaCovid19CuratedAccountsWtf - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.IndiaCovid19CuratedAccountsWtf - override val toFsName: String = "IndiaCovid19CuratedAccountsWtf" - } - - case object PeoplePlusPlus extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.PeoplePlusPlus - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.PeoplePlusPlus - override val toFsName: String = "PeoplePlusPlus" - } - - case object TweetNotificationRecs extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.TweetNotificationRecs - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.TweetNotificationRecs - override val toFsName: String = "TweetNotificationRecs" - } - - case object ProfileDeviceFollow extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.ProfileDeviceFollow - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.ProfileDeviceFollow - override val toFsName: String = "ProfileDeviceFollow" - } - - case object RecosBackfill extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.RecosBackfill - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.RecosBackfill - override val toFsName: String = "RecosBackfill" - } - - case object HtlSpaceHosts extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.HtlSpaceHosts - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.HtlSpaceHosts - override val toFsName: String = "HtlSpaceHosts" - } - - case object PostNuxFollowTask extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.PostNuxFollowTask - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.PostNuxFollowTask - override val toFsName: String = "PostNuxFollowTask" - } - - case object TopicLandingPage extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.TopicLandingPage - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.TopicLandingPage - override val toFsName: String = "TopicLandingPage" - } - - case object UserTypeaheadPrefetch extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.UserTypeaheadPrefetch - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.UserTypeaheadPrefetch - override val toFsName: String = "UserTypeaheadPrefetch" - } - - case object HomeTimelineRelatableAccounts extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.HomeTimelineRelatableAccounts - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.HomeTimelineRelatableAccounts - override val toFsName: String = "HomeTimelineRelatableAccounts" - } - - case object NuxGeoCategory extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.NuxGeoCategory - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.NuxGeoCategory - override val toFsName: String = "NuxGeoCategory" - } - - case object NuxInterestsCategory extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.NuxInterestsCategory - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.NuxInterestsCategory - override val toFsName: String = "NuxInterestsCategory" - } - - case object TopArticles extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.TopArticles - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.TopArticles - override val toFsName: String = "TopArticles" - } - - case object NuxPymkCategory extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.NuxPymkCategory - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.NuxPymkCategory - override val toFsName: String = "NuxPymkCategory" - } - - case object HomeTimelineTweetRecs extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.HomeTimelineTweetRecs - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.HomeTimelineTweetRecs - override val toFsName: String = "HomeTimelineTweetRecs" - } - - case object HtlBulkFriendFollows extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.HtlBulkFriendFollows - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.HtlBulkFriendFollows - override val toFsName: String = "HtlBulkFriendFollows" - } - - case object NuxAutoFollow extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.NuxAutoFollow - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.NuxAutoFollow - override val toFsName: String = "NuxAutoFollow" - } - - case object SearchBonusFollow extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.SearchBonusFollow - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.SearchBonusFollow - override val toFsName: String = "SearchBonusFollow" - } - - case object ContentRecommender extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.ContentRecommender - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.ContentRecommender - override val toFsName: String = "ContentRecommender" - } - - case object HomeTimelineReverseChron extends DisplayLocation { - override val toThrift: TDisplayLocation = TDisplayLocation.HomeTimelineReverseChron - override val toOfflineThrift: TOfflineDisplayLocation = - TOfflineDisplayLocation.HomeTimelineReverseChron - override val toFsName: String = "HomeTimelineReverseChron" - } - - def fromThrift(displayLocation: TDisplayLocation): DisplayLocation = displayLocation match { - case TDisplayLocation.ProfileSidebar => ProfileSidebar - case TDisplayLocation.HomeTimeline => HomeTimeline - case TDisplayLocation.MagicRecs => MagicRecs - case TDisplayLocation.AbUploadInjection => AbUploadInjection - case TDisplayLocation.RuxLandingPage => RuxLandingPage - case TDisplayLocation.ProfileBonusFollow => ProfileBonusFollow - case TDisplayLocation.ElectionExploreWtf => ElectionExploreWtf - case TDisplayLocation.ClusterFollow => ClusterFollow - case TDisplayLocation.HtlBonusFollow => HtlBonusFollow - case TDisplayLocation.ReactiveFollow => ReactiveFollow - case TDisplayLocation.TopicLandingPageHeader => TopicLandingPageHeader - case TDisplayLocation.NewUserSarusBackfill => NewUserSarusBackfill - case TDisplayLocation.NuxPymk => NuxPymk - case TDisplayLocation.NuxInterests => NuxInterests - case TDisplayLocation.NuxTopicBonusFollow => NuxTopicBonusFollow - case TDisplayLocation.ExploreTab => ExploreTab - case TDisplayLocation.Sidebar => Sidebar - case TDisplayLocation.CampaignForm => CampaignForm - case TDisplayLocation.ProfileTopFollowers => ProfileTopFollowers - case TDisplayLocation.ProfileTopFollowing => ProfileTopFollowing - case TDisplayLocation.RuxPymk => RuxPymk - case TDisplayLocation.IndiaCovid19CuratedAccountsWtf => IndiaCovid19CuratedAccountsWtf - case TDisplayLocation.PeoplePlusPlus => PeoplePlusPlus - case TDisplayLocation.TweetNotificationRecs => TweetNotificationRecs - case TDisplayLocation.ProfileDeviceFollow => ProfileDeviceFollow - case TDisplayLocation.RecosBackfill => RecosBackfill - case TDisplayLocation.HtlSpaceHosts => HtlSpaceHosts - case TDisplayLocation.PostNuxFollowTask => PostNuxFollowTask - case TDisplayLocation.TopicLandingPage => TopicLandingPage - case TDisplayLocation.UserTypeaheadPrefetch => UserTypeaheadPrefetch - case TDisplayLocation.HomeTimelineRelatableAccounts => HomeTimelineRelatableAccounts - case TDisplayLocation.NuxGeoCategory => NuxGeoCategory - case TDisplayLocation.NuxInterestsCategory => NuxInterestsCategory - case TDisplayLocation.TopArticles => TopArticles - case TDisplayLocation.NuxPymkCategory => NuxPymkCategory - case TDisplayLocation.HomeTimelineTweetRecs => HomeTimelineTweetRecs - case TDisplayLocation.HtlBulkFriendFollows => HtlBulkFriendFollows - case TDisplayLocation.NuxAutoFollow => NuxAutoFollow - case TDisplayLocation.SearchBonusFollow => SearchBonusFollow - case TDisplayLocation.ContentRecommender => ContentRecommender - case TDisplayLocation.HomeTimelineReverseChron => HomeTimelineReverseChron - case TDisplayLocation.EnumUnknownDisplayLocation(i) => - throw new UnknownDisplayLocationException( - s"Unknown display location thrift enum with value: ${i}") - } - - def fromOfflineThrift(displayLocation: TOfflineDisplayLocation): DisplayLocation = - displayLocation match { - case TOfflineDisplayLocation.ProfileSidebar => ProfileSidebar - case TOfflineDisplayLocation.HomeTimeline => HomeTimeline - case TOfflineDisplayLocation.MagicRecs => MagicRecs - case TOfflineDisplayLocation.AbUploadInjection => AbUploadInjection - case TOfflineDisplayLocation.RuxLandingPage => RuxLandingPage - case TOfflineDisplayLocation.ProfileBonusFollow => ProfileBonusFollow - case TOfflineDisplayLocation.ElectionExploreWtf => ElectionExploreWtf - case TOfflineDisplayLocation.ClusterFollow => ClusterFollow - case TOfflineDisplayLocation.HtlBonusFollow => HtlBonusFollow - case TOfflineDisplayLocation.TopicLandingPageHeader => TopicLandingPageHeader - case TOfflineDisplayLocation.NewUserSarusBackfill => NewUserSarusBackfill - case TOfflineDisplayLocation.NuxPymk => NuxPymk - case TOfflineDisplayLocation.NuxInterests => NuxInterests - case TOfflineDisplayLocation.NuxTopicBonusFollow => NuxTopicBonusFollow - case TOfflineDisplayLocation.ExploreTab => ExploreTab - case TOfflineDisplayLocation.ReactiveFollow => ReactiveFollow - case TOfflineDisplayLocation.Sidebar => Sidebar - case TOfflineDisplayLocation.CampaignForm => CampaignForm - case TOfflineDisplayLocation.ProfileTopFollowers => ProfileTopFollowers - case TOfflineDisplayLocation.ProfileTopFollowing => ProfileTopFollowing - case TOfflineDisplayLocation.RuxPymk => RuxPymk - case TOfflineDisplayLocation.IndiaCovid19CuratedAccountsWtf => IndiaCovid19CuratedAccountsWtf - case TOfflineDisplayLocation.PeoplePlusPlus => PeoplePlusPlus - case TOfflineDisplayLocation.TweetNotificationRecs => TweetNotificationRecs - case TOfflineDisplayLocation.ProfileDeviceFollow => ProfileDeviceFollow - case TOfflineDisplayLocation.RecosBackfill => RecosBackfill - case TOfflineDisplayLocation.HtlSpaceHosts => HtlSpaceHosts - case TOfflineDisplayLocation.PostNuxFollowTask => PostNuxFollowTask - case TOfflineDisplayLocation.TopicLandingPage => TopicLandingPage - case TOfflineDisplayLocation.UserTypeaheadPrefetch => UserTypeaheadPrefetch - case TOfflineDisplayLocation.HomeTimelineRelatableAccounts => HomeTimelineRelatableAccounts - case TOfflineDisplayLocation.NuxGeoCategory => NuxGeoCategory - case TOfflineDisplayLocation.NuxInterestsCategory => NuxInterestsCategory - case TOfflineDisplayLocation.TopArticles => TopArticles - case TOfflineDisplayLocation.NuxPymkCategory => NuxPymkCategory - case TOfflineDisplayLocation.HomeTimelineTweetRecs => HomeTimelineTweetRecs - case TOfflineDisplayLocation.HtlBulkFriendFollows => HtlBulkFriendFollows - case TOfflineDisplayLocation.NuxAutoFollow => NuxAutoFollow - case TOfflineDisplayLocation.SearchBonusFollow => SearchBonusFollow - case TOfflineDisplayLocation.ContentRecommender => ContentRecommender - case TOfflineDisplayLocation.HomeTimelineReverseChron => HomeTimelineReverseChron - case TOfflineDisplayLocation.EnumUnknownOfflineDisplayLocation(i) => - throw new UnknownDisplayLocationException( - s"Unknown offline display location thrift enum with value: ${i}") - } -} - -class UnknownDisplayLocationException(message: String) extends Exception(message) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/EngagementType.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/EngagementType.scala deleted file mode 100644 index b12a4404c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/EngagementType.scala +++ /dev/null @@ -1,62 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.thriftscala.{EngagementType => TEngagementType} -import com.twitter.follow_recommendations.logging.thriftscala.{ - EngagementType => OfflineEngagementType -} -sealed trait EngagementType { - def toThrift: TEngagementType - def toOfflineThrift: OfflineEngagementType -} - -object EngagementType { - object Click extends EngagementType { - override val toThrift: TEngagementType = TEngagementType.Click - - override val toOfflineThrift: OfflineEngagementType = OfflineEngagementType.Click - } - object Like extends EngagementType { - override val toThrift: TEngagementType = TEngagementType.Like - - override val toOfflineThrift: OfflineEngagementType = OfflineEngagementType.Like - } - object Mention extends EngagementType { - override val toThrift: TEngagementType = TEngagementType.Mention - - override val toOfflineThrift: OfflineEngagementType = OfflineEngagementType.Mention - } - object Retweet extends EngagementType { - override val toThrift: TEngagementType = TEngagementType.Retweet - - override val toOfflineThrift: OfflineEngagementType = OfflineEngagementType.Retweet - } - object ProfileView extends EngagementType { - override val toThrift: TEngagementType = TEngagementType.ProfileView - - override val toOfflineThrift: OfflineEngagementType = OfflineEngagementType.ProfileView - } - - def fromThrift(engagementType: TEngagementType): EngagementType = engagementType match { - case TEngagementType.Click => Click - case TEngagementType.Like => Like - case TEngagementType.Mention => Mention - case TEngagementType.Retweet => Retweet - case TEngagementType.ProfileView => ProfileView - case TEngagementType.EnumUnknownEngagementType(i) => - throw new UnknownEngagementTypeException( - s"Unknown engagement type thrift enum with value: ${i}") - } - - def fromOfflineThrift(engagementType: OfflineEngagementType): EngagementType = - engagementType match { - case OfflineEngagementType.Click => Click - case OfflineEngagementType.Like => Like - case OfflineEngagementType.Mention => Mention - case OfflineEngagementType.Retweet => Retweet - case OfflineEngagementType.ProfileView => ProfileView - case OfflineEngagementType.EnumUnknownEngagementType(i) => - throw new UnknownEngagementTypeException( - s"Unknown engagement type offline thrift enum with value: ${i}") - } -} -class UnknownEngagementTypeException(message: String) extends Exception(message) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FilterReason.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FilterReason.scala deleted file mode 100644 index 86b496776..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FilterReason.scala +++ /dev/null @@ -1,133 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -sealed trait FilterReason { - def reason: String -} - -object FilterReason { - - case object NoReason extends FilterReason { - override val reason: String = "no_reason" - } - - case class ParamReason(paramName: String) extends FilterReason { - override val reason: String = s"param_$paramName" - } - - case object ExcludedId extends FilterReason { - override val reason: String = "excluded_id_from_request" - } - - case object ProfileSidebarBlacklist extends FilterReason { - override val reason: String = "profile_sidebar_blacklisted_id" - } - - case object CuratedAccountsCompetitorList extends FilterReason { - override val reason: String = "curated_blacklisted_id" - } - - case class InvalidRelationshipTypes(relationshipTypes: String) extends FilterReason { - override val reason: String = s"invalid_relationship_types $relationshipTypes" - } - - case object ProfileId extends FilterReason { - override val reason: String = "candidate_has_same_id_as_profile" - } - - case object DismissedId extends FilterReason { - override val reason: String = s"dismissed_candidate" - } - - case object OptedOutId extends FilterReason { - override val reason: String = s"candidate_opted_out_from_criteria_in_request" - } - - // gizmoduck predicates - case object NoUser extends FilterReason { - override val reason: String = "no_user_result_from_gizmoduck" - } - - case object AddressBookUndiscoverable extends FilterReason { - override val reason: String = "not_discoverable_via_address_book" - } - - case object PhoneBookUndiscoverable extends FilterReason { - override val reason: String = "not_discoverable_via_phone_book" - } - - case object Deactivated extends FilterReason { - override val reason: String = "deactivated" - } - - case object Suspended extends FilterReason { - override val reason: String = "suspended" - } - - case object Restricted extends FilterReason { - override val reason: String = "restricted" - } - - case object NsfwUser extends FilterReason { - override val reason: String = "nsfwUser" - } - - case object NsfwAdmin extends FilterReason { - override val reason: String = "nsfwAdmin" - } - - case object HssSignal extends FilterReason { - override val reason: String = "hssSignal" - } - - case object IsProtected extends FilterReason { - override val reason: String = "isProtected" - } - - case class CountryTakedown(countryCode: String) extends FilterReason { - override val reason: String = s"takedown_in_$countryCode" - } - - case object Blink extends FilterReason { - override val reason: String = "blink" - } - - case object AlreadyFollowed extends FilterReason { - override val reason: String = "already_followed" - } - - case object InvalidRelationship extends FilterReason { - override val reason: String = "invalid_relationship" - } - - case object NotFollowingTargetUser extends FilterReason { - override val reason: String = "not_following_target_user" - } - - case object CandidateSideHoldback extends FilterReason { - override val reason: String = "candidate_side_holdback" - } - - case object Inactive extends FilterReason { - override val reason: String = "inactive" - } - - case object MissingRecommendabilityData extends FilterReason { - override val reason: String = "missing_recommendability_data" - } - - case object HighTweetVelocity extends FilterReason { - override val reason: String = "high_tweet_velocity" - } - - case object AlreadyRecommended extends FilterReason { - override val reason: String = "already_recommended" - } - - case object MinStateNotMet extends FilterReason { - override val reason: String = "min_state_user_not_met" - } - - case object FailOpen extends FilterReason { - override val reason: String = "fail_open" - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FlowContext.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FlowContext.scala deleted file mode 100644 index 15e36321e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FlowContext.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.{thriftscala => t} - -case class FlowContext(steps: Seq[RecommendationStep]) { - - def toThrift: t.FlowContext = t.FlowContext(steps = steps.map(_.toThrift)) - - def toOfflineThrift: offline.OfflineFlowContext = - offline.OfflineFlowContext(steps = steps.map(_.toOfflineThrift)) -} - -object FlowContext { - - def fromThrift(flowContext: t.FlowContext): FlowContext = { - FlowContext(steps = flowContext.steps.map(RecommendationStep.fromThrift)) - } - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FlowRecommendation.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FlowRecommendation.scala deleted file mode 100644 index 118ff258d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/FlowRecommendation.scala +++ /dev/null @@ -1,23 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.{thriftscala => t} - -case class FlowRecommendation(userId: Long) { - - def toThrift: t.FlowRecommendation = - t.FlowRecommendation(userId = userId) - - def toOfflineThrift: offline.OfflineFlowRecommendation = - offline.OfflineFlowRecommendation(userId = userId) - -} - -object FlowRecommendation { - def fromThrift(flowRecommendation: t.FlowRecommendation): FlowRecommendation = { - FlowRecommendation( - userId = flowRecommendation.userId - ) - } - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/GeohashAndCountryCode.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/GeohashAndCountryCode.scala deleted file mode 100644 index 782d3fc9e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/GeohashAndCountryCode.scala +++ /dev/null @@ -1,3 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -case class GeohashAndCountryCode(geohash: Option[String], countryCode: Option[String]) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasAdMetadata.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasAdMetadata.scala deleted file mode 100644 index 57979e376..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasAdMetadata.scala +++ /dev/null @@ -1,23 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.adserver.{thriftscala => t} - -case class AdMetadata( - insertPosition: Int, - // use original ad impression info to avoid losing data in domain model translations - adImpression: t.AdImpression) - -trait HasAdMetadata { - - def adMetadata: Option[AdMetadata] - - def adImpression: Option[t.AdImpression] = { - adMetadata.map(_.adImpression) - } - - def insertPosition: Option[Int] = { - adMetadata.map(_.insertPosition) - } - - def isPromotedAccount: Boolean = adMetadata.isDefined -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasByfSeedUserIds.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasByfSeedUserIds.scala deleted file mode 100644 index d4cbdcee8..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasByfSeedUserIds.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasByfSeedUserIds { - def byfSeedUserIds: Option[Seq[Long]] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDataRecord.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDataRecord.scala deleted file mode 100644 index 4e7047b4e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDataRecord.scala +++ /dev/null @@ -1,86 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.thriftscala.DebugDataRecord -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.FeatureContext -import com.twitter.util.Try -import com.twitter.util.logging.Logging -import scala.collection.convert.ImplicitConversions._ - -// contains the standard dataRecord struct, and the debug version if required -case class RichDataRecord( - dataRecord: Option[DataRecord] = None, - debugDataRecord: Option[DebugDataRecord] = None, -) - -trait HasDataRecord extends Logging { - def dataRecord: Option[RichDataRecord] - - def toDebugDataRecord(dr: DataRecord, featureContext: FeatureContext): DebugDataRecord = { - - val binaryFeatures: Option[Set[String]] = if (dr.isSetBinaryFeatures) { - Some(dr.getBinaryFeatures.flatMap { id => - Try(featureContext.getFeature(id).getFeatureName).toOption - }.toSet) - } else None - - val continuousFeatures: Option[Map[String, Double]] = if (dr.isSetContinuousFeatures) { - Some(dr.getContinuousFeatures.flatMap { - case (id, value) => - Try(featureContext.getFeature(id).getFeatureName).toOption.map { id => - id -> value.toDouble - } - }.toMap) - } else None - - val discreteFeatures: Option[Map[String, Long]] = if (dr.isSetDiscreteFeatures) { - Some(dr.getDiscreteFeatures.flatMap { - case (id, value) => - Try(featureContext.getFeature(id).getFeatureName).toOption.map { id => - id -> value.toLong - } - }.toMap) - } else None - - val stringFeatures: Option[Map[String, String]] = if (dr.isSetStringFeatures) { - Some(dr.getStringFeatures.flatMap { - case (id, value) => - Try(featureContext.getFeature(id).getFeatureName).toOption.map { id => - id -> value - } - }.toMap) - } else None - - val sparseBinaryFeatures: Option[Map[String, Set[String]]] = if (dr.isSetSparseBinaryFeatures) { - Some(dr.getSparseBinaryFeatures.flatMap { - case (id, values) => - Try(featureContext.getFeature(id).getFeatureName).toOption.map { id => - id -> values.toSet - } - }.toMap) - } else None - - val sparseContinuousFeatures: Option[Map[String, Map[String, Double]]] = - if (dr.isSetSparseContinuousFeatures) { - Some(dr.getSparseContinuousFeatures.flatMap { - case (id, values) => - Try(featureContext.getFeature(id).getFeatureName).toOption.map { id => - id -> values.map { - case (str, value) => - str -> value.toDouble - }.toMap - } - }.toMap) - } else None - - DebugDataRecord( - binaryFeatures = binaryFeatures, - continuousFeatures = continuousFeatures, - discreteFeatures = discreteFeatures, - stringFeatures = stringFeatures, - sparseBinaryFeatures = sparseBinaryFeatures, - sparseContinuousFeatures = sparseContinuousFeatures, - ) - } - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDebugOptions.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDebugOptions.scala deleted file mode 100644 index 0956ca34e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDebugOptions.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.thriftscala.DebugParams - -case class DebugOptions( - randomizationSeed: Option[Long] = None, - fetchDebugInfo: Boolean = false, - doNotLog: Boolean = false) - -object DebugOptions { - def fromDebugParamsThrift(debugParams: DebugParams): DebugOptions = { - DebugOptions( - debugParams.randomizationSeed, - debugParams.includeDebugInfoInResults.getOrElse(false), - debugParams.doNotLog.getOrElse(false) - ) - } -} - -trait HasDebugOptions { - def debugOptions: Option[DebugOptions] - - def getRandomizationSeed: Option[Long] = debugOptions.flatMap(_.randomizationSeed) - - def fetchDebugInfo: Option[Boolean] = debugOptions.map(_.fetchDebugInfo) -} - -trait HasFrsDebugOptions { - def frsDebugOptions: Option[DebugOptions] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDismissedUserIds.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDismissedUserIds.scala deleted file mode 100644 index 3f2154992..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDismissedUserIds.scala +++ /dev/null @@ -1,6 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasDismissedUserIds { - // user ids that are recently followed by the target user - def dismissedUserIds: Option[Seq[Long]] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDisplayLocation.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDisplayLocation.scala deleted file mode 100644 index e74ae83e1..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasDisplayLocation.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasDisplayLocation { - def displayLocation: DisplayLocation -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasEngagements.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasEngagements.scala deleted file mode 100644 index de59e4479..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasEngagements.scala +++ /dev/null @@ -1,7 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasEngagements { - - def engagements: Seq[EngagementType] - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasExcludedUserIds.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasExcludedUserIds.scala deleted file mode 100644 index 3addcef83..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasExcludedUserIds.scala +++ /dev/null @@ -1,6 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasExcludedUserIds { - // user ids that are going to be excluded from recommendations - def excludedUserIds: Seq[Long] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasGeohashAndCountryCode.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasGeohashAndCountryCode.scala deleted file mode 100644 index a4364bbf4..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasGeohashAndCountryCode.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasGeohashAndCountryCode { - def geohashAndCountryCode: Option[GeohashAndCountryCode] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInfoPerRankingStage.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInfoPerRankingStage.scala deleted file mode 100644 index c4d277412..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInfoPerRankingStage.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasInfoPerRankingStage { - def infoPerRankingStage: Option[scala.collection.Map[String, RankingInfo]] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInterestIds.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInterestIds.scala deleted file mode 100644 index 69f97b673..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInterestIds.scala +++ /dev/null @@ -1,11 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasCustomInterests { - def customInterests: Option[Seq[String]] -} - -trait HasUttInterests { - def uttInterestIds: Option[Seq[Long]] -} - -trait HasInterestIds extends HasCustomInterests with HasUttInterests {} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInvalidRelationshipUserIds.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInvalidRelationshipUserIds.scala deleted file mode 100644 index 3cf3f66db..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasInvalidRelationshipUserIds.scala +++ /dev/null @@ -1,6 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasInvalidRelationshipUserIds { - // user ids that have invalid relationship with the target user - def invalidRelationshipUserIds: Option[Set[Long]] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasIsSoftUser.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasIsSoftUser.scala deleted file mode 100644 index 8cf1532ce..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasIsSoftUser.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasIsSoftUser { - def isSoftUser: Boolean -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasMutualFollowedUserIds.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasMutualFollowedUserIds.scala deleted file mode 100644 index c5e1e16cc..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasMutualFollowedUserIds.scala +++ /dev/null @@ -1,10 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -// intersection of recent followers and followed by -trait HasMutualFollowedUserIds extends HasRecentFollowedUserIds with HasRecentFollowedByUserIds { - - lazy val recentMutualFollows: Seq[Long] = - recentFollowedUserIds.getOrElse(Nil).intersect(recentFollowedByUserIds.getOrElse(Nil)) - - lazy val numRecentMutualFollows: Int = recentMutualFollows.size -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasPreviousRecommendationsContext.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasPreviousRecommendationsContext.scala deleted file mode 100644 index 2480faaad..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasPreviousRecommendationsContext.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasPreviousRecommendationsContext { - - def previouslyRecommendedUserIDs: Set[Long] - - def previouslyFollowedUserIds: Set[Long] - - def skippedFollows: Set[Long] = { - previouslyRecommendedUserIDs.diff(previouslyFollowedUserIds) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasProfileId.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasProfileId.scala deleted file mode 100644 index 10cd7c02f..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasProfileId.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasProfileId { - def profileId: Long -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasQualityFactor.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasQualityFactor.scala deleted file mode 100644 index 96527b2ba..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasQualityFactor.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasQualityFactor { - def qualityFactor: Option[Double] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedByUserIds.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedByUserIds.scala deleted file mode 100644 index bc15e8bd8..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedByUserIds.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasRecentFollowedByUserIds { - // user ids that have recently followed the target user; target user has been "followed by" them. - def recentFollowedByUserIds: Option[Seq[Long]] - - lazy val numRecentFollowedByUserIds: Int = recentFollowedByUserIds.map(_.size).getOrElse(0) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedUserIds.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedUserIds.scala deleted file mode 100644 index 67ada7c66..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedUserIds.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasRecentFollowedUserIds { - // user ids that are recently followed by the target user - def recentFollowedUserIds: Option[Seq[Long]] - - // user ids that are recently followed by the target user in set data-structure - lazy val recentFollowedUserIdsSet: Option[Set[Long]] = recentFollowedUserIds match { - case Some(users) => Some(users.toSet) - case None => Some(Set.empty) - } - - lazy val numRecentFollowedUserIds: Int = recentFollowedUserIds.map(_.size).getOrElse(0) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedUserIdsWithTime.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedUserIdsWithTime.scala deleted file mode 100644 index 7e3cba4a7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentFollowedUserIdsWithTime.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasRecentFollowedUserIdsWithTime { - // user ids that are recently followed by the target user - def recentFollowedUserIdsWithTime: Option[Seq[UserIdWithTimestamp]] - - lazy val numRecentFollowedUserIdsWithTime: Int = - recentFollowedUserIdsWithTime.map(_.size).getOrElse(0) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentlyEngagedUserIds.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentlyEngagedUserIds.scala deleted file mode 100644 index 44420ec27..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecentlyEngagedUserIds.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasRecentlyEngagedUserIds { - val recentlyEngagedUserIds: Option[Seq[RecentlyEngagedUserId]] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecommendationFlowIdentifier.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecommendationFlowIdentifier.scala deleted file mode 100644 index 5706c7a29..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasRecommendationFlowIdentifier.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasRecommendationFlowIdentifier { - def recommendationFlowIdentifier: Option[String] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasScores.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasScores.scala deleted file mode 100644 index e8a6698ee..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasScores.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasScores { - def scores: Option[Scores] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasSimilarToContext.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasSimilarToContext.scala deleted file mode 100644 index bbe2ac258..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasSimilarToContext.scala +++ /dev/null @@ -1,7 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasSimilarToContext { - - // user ids that are used to generate similar to recommendations - def similarToUserIds: Seq[Long] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasTopicId.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasTopicId.scala deleted file mode 100644 index 4bd6e63e7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasTopicId.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -trait HasTopicId { - def topicId: Long -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasUserCandidateSourceDetails.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasUserCandidateSourceDetails.scala deleted file mode 100644 index e0e363449..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasUserCandidateSourceDetails.scala +++ /dev/null @@ -1,162 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.hermit.ml.models.Feature -import com.twitter.hermit.model.Algorithm -import com.twitter.hermit.model.Algorithm.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier - -/** - * Used to keep track of a candidate's source not so much as a feature but for filtering candidate - * from specific sources (eg. GizmoduckPredicate) - */ -trait HasUserCandidateSourceDetails { candidateUser: CandidateUser => - def userCandidateSourceDetails: Option[UserCandidateSourceDetails] - - def getAlgorithm: Algorithm = { - val algorithm = for { - details <- userCandidateSourceDetails - identifier <- details.primaryCandidateSource - algorithm <- Algorithm.withNameOpt(identifier.name) - } yield algorithm - - algorithm.getOrElse(throw new Exception("Algorithm missing on candidate user!")) - } - - def getAllAlgorithms: Seq[Algorithm] = { - getCandidateSources.keys - .flatMap(identifier => Algorithm.withNameOpt(identifier.name)).toSeq - } - - def getAddressBookMetadata: Option[AddressBookMetadata] = { - userCandidateSourceDetails.flatMap(_.addressBookMetadata) - } - - def getCandidateSources: Map[CandidateSourceIdentifier, Option[Double]] = { - userCandidateSourceDetails.map(_.candidateSourceScores).getOrElse(Map.empty) - } - - def getCandidateRanks: Map[CandidateSourceIdentifier, Int] = { - userCandidateSourceDetails.map(_.candidateSourceRanks).getOrElse(Map.empty) - } - - def getCandidateFeatures: Map[CandidateSourceIdentifier, Seq[Feature]] = { - userCandidateSourceDetails.map(_.candidateSourceFeatures).getOrElse(Map.empty) - } - - def getPrimaryCandidateSource: Option[CandidateSourceIdentifier] = { - userCandidateSourceDetails.flatMap(_.primaryCandidateSource) - } - - def withCandidateSource(source: CandidateSourceIdentifier): CandidateUser = { - withCandidateSourceAndScore(source, candidateUser.score) - } - - def withCandidateSourceAndScore( - source: CandidateSourceIdentifier, - score: Option[Double] - ): CandidateUser = { - withCandidateSourceScoreAndFeatures(source, score, Nil) - } - - def withCandidateSourceAndFeatures( - source: CandidateSourceIdentifier, - features: Seq[Feature] - ): CandidateUser = { - withCandidateSourceScoreAndFeatures(source, candidateUser.score, features) - } - - def withCandidateSourceScoreAndFeatures( - source: CandidateSourceIdentifier, - score: Option[Double], - features: Seq[Feature] - ): CandidateUser = { - val candidateSourceDetails = - candidateUser.userCandidateSourceDetails - .map { details => - details.copy( - primaryCandidateSource = Some(source), - candidateSourceScores = details.candidateSourceScores + (source -> score), - candidateSourceFeatures = details.candidateSourceFeatures + (source -> features) - ) - }.getOrElse( - UserCandidateSourceDetails( - Some(source), - Map(source -> score), - Map.empty, - None, - Map(source -> features))) - candidateUser.copy( - userCandidateSourceDetails = Some(candidateSourceDetails) - ) - } - - def addCandidateSourceScoresMap( - scoreMap: Map[CandidateSourceIdentifier, Option[Double]] - ): CandidateUser = { - val candidateSourceDetails = candidateUser.userCandidateSourceDetails - .map { details => - details.copy(candidateSourceScores = details.candidateSourceScores ++ scoreMap) - }.getOrElse(UserCandidateSourceDetails(scoreMap.keys.headOption, scoreMap, Map.empty, None)) - candidateUser.copy( - userCandidateSourceDetails = Some(candidateSourceDetails) - ) - } - - def addCandidateSourceRanksMap( - rankMap: Map[CandidateSourceIdentifier, Int] - ): CandidateUser = { - val candidateSourceDetails = candidateUser.userCandidateSourceDetails - .map { details => - details.copy(candidateSourceRanks = details.candidateSourceRanks ++ rankMap) - }.getOrElse(UserCandidateSourceDetails(rankMap.keys.headOption, Map.empty, rankMap, None)) - candidateUser.copy( - userCandidateSourceDetails = Some(candidateSourceDetails) - ) - } - - def addInfoPerRankingStage( - rankingStage: String, - scores: Option[Scores], - rank: Int - ): CandidateUser = { - val scoresOpt: Option[Scores] = scores.orElse(candidateUser.scores) - val originalInfoPerRankingStage = - candidateUser.infoPerRankingStage.getOrElse(Map[String, RankingInfo]()) - candidateUser.copy( - infoPerRankingStage = - Some(originalInfoPerRankingStage + (rankingStage -> RankingInfo(scoresOpt, Some(rank)))) - ) - } - - def addAddressBookMetadataIfAvailable( - candidateSources: Seq[CandidateSourceIdentifier] - ): CandidateUser = { - - val addressBookMetadata = AddressBookMetadata( - inForwardPhoneBook = - candidateSources.contains(AddressBookMetadata.ForwardPhoneBookCandidateSource), - inReversePhoneBook = - candidateSources.contains(AddressBookMetadata.ReversePhoneBookCandidateSource), - inForwardEmailBook = - candidateSources.contains(AddressBookMetadata.ForwardEmailBookCandidateSource), - inReverseEmailBook = - candidateSources.contains(AddressBookMetadata.ReverseEmailBookCandidateSource) - ) - - val newCandidateSourceDetails = candidateUser.userCandidateSourceDetails - .map { details => - details.copy(addressBookMetadata = Some(addressBookMetadata)) - }.getOrElse( - UserCandidateSourceDetails( - None, - Map.empty, - Map.empty, - Some(addressBookMetadata), - Map.empty)) - - candidateUser.copy( - userCandidateSourceDetails = Some(newCandidateSourceDetails) - ) - } - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasUserState.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasUserState.scala deleted file mode 100644 index bf0df46f7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasUserState.scala +++ /dev/null @@ -1,7 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.core_workflows.user_model.thriftscala.UserState - -trait HasUserState { - def userState: Option[UserState] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasWtfImpressions.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasWtfImpressions.scala deleted file mode 100644 index d840ed6fd..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/HasWtfImpressions.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.util.Time - -trait HasWtfImpressions { - - def wtfImpressions: Option[Seq[WtfImpression]] - - lazy val numWtfImpressions: Int = wtfImpressions.map(_.size).getOrElse(0) - - lazy val candidateImpressions: Map[Long, WtfImpression] = wtfImpressions - .map { imprMap => - imprMap.map { i => - i.candidateId -> i - }.toMap - }.getOrElse(Map.empty) - - lazy val latestImpressionTime: Time = { - if (wtfImpressions.exists(_.nonEmpty)) { - wtfImpressions.get.map(_.latestTime).max - } else Time.Top - } - - def getCandidateImpressionCounts(id: Long): Option[Int] = - candidateImpressions.get(id).map(_.counts) - - def getCandidateLatestTime(id: Long): Option[Time] = { - candidateImpressions.get(id).map(_.latestTime) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/OptimusRequest.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/OptimusRequest.scala deleted file mode 100644 index df4c228e1..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/OptimusRequest.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.timelines.configapi.HasParams - -/** -Convenience trait to group together all traits needed for optimus ranking - */ -trait OptimusRequest - extends HasParams - with HasClientContext - with HasDisplayLocation - with HasInterestIds - with HasDebugOptions - with HasPreviousRecommendationsContext {} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Product.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Product.scala deleted file mode 100644 index f37cff56e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Product.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.product_mixer.core.model.common.identifier.ProductIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.{Product => ProductMixerProduct} - -object Product { - case object MagicRecs extends ProductMixerProduct { - override val identifier: ProductIdentifier = ProductIdentifier("MagicRecs") - override val stringCenterProject: Option[String] = Some("people-discovery") - } - - case object PlaceholderProductMixerProduct extends ProductMixerProduct { - override val identifier: ProductIdentifier = ProductIdentifier("PlaceholderProductMixerProduct") - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RankingInfo.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RankingInfo.scala deleted file mode 100644 index 02eb46b5a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RankingInfo.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.{thriftscala => t} -import com.twitter.follow_recommendations.logging.{thriftscala => offline} - -case class RankingInfo( - scores: Option[Scores], - rank: Option[Int]) { - - def toThrift: t.RankingInfo = { - t.RankingInfo(scores.map(_.toThrift), rank) - } - - def toOfflineThrift: offline.RankingInfo = { - offline.RankingInfo(scores.map(_.toOfflineThrift), rank) - } -} - -object RankingInfo { - - def fromThrift(rankingInfo: t.RankingInfo): RankingInfo = { - RankingInfo( - scores = rankingInfo.scores.map(Scores.fromThrift), - rank = rankingInfo.rank - ) - } - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Reason.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Reason.scala deleted file mode 100644 index b7c9c6c75..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Reason.scala +++ /dev/null @@ -1,206 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.{thriftscala => t} -import com.twitter.follow_recommendations.logging.{thriftscala => offline} - -case class FollowProof(followedBy: Seq[Long], numIds: Int) { - def toThrift: t.FollowProof = { - t.FollowProof(followedBy, numIds) - } - - def toOfflineThrift: offline.FollowProof = offline.FollowProof(followedBy, numIds) -} - -object FollowProof { - - def fromThrift(proof: t.FollowProof): FollowProof = { - FollowProof(proof.userIds, proof.numIds) - } -} - -case class SimilarToProof(similarTo: Seq[Long]) { - def toThrift: t.SimilarToProof = { - t.SimilarToProof(similarTo) - } - - def toOfflineThrift: offline.SimilarToProof = offline.SimilarToProof(similarTo) -} - -object SimilarToProof { - def fromThrift(proof: t.SimilarToProof): SimilarToProof = { - SimilarToProof(proof.userIds) - } -} - -case class PopularInGeoProof(location: String) { - def toThrift: t.PopularInGeoProof = { - t.PopularInGeoProof(location) - } - - def toOfflineThrift: offline.PopularInGeoProof = offline.PopularInGeoProof(location) -} - -object PopularInGeoProof { - - def fromThrift(proof: t.PopularInGeoProof): PopularInGeoProof = { - PopularInGeoProof(proof.location) - } -} - -case class TttInterestProof(interestId: Long, interestDisplayName: String) { - def toThrift: t.TttInterestProof = { - t.TttInterestProof(interestId, interestDisplayName) - } - - def toOfflineThrift: offline.TttInterestProof = - offline.TttInterestProof(interestId, interestDisplayName) -} - -object TttInterestProof { - - def fromThrift(proof: t.TttInterestProof): TttInterestProof = { - TttInterestProof(proof.interestId, proof.interestDisplayName) - } -} - -case class TopicProof(topicId: Long) { - def toThrift: t.TopicProof = { - t.TopicProof(topicId) - } - - def toOfflineThrift: offline.TopicProof = - offline.TopicProof(topicId) -} - -object TopicProof { - def fromThrift(proof: t.TopicProof): TopicProof = { - TopicProof(proof.topicId) - } -} - -case class CustomInterest(query: String) { - def toThrift: t.CustomInterestProof = { - t.CustomInterestProof(query) - } - - def toOfflineThrift: offline.CustomInterestProof = - offline.CustomInterestProof(query) -} - -object CustomInterest { - def fromThrift(proof: t.CustomInterestProof): CustomInterest = { - CustomInterest(proof.query) - } -} - -case class TweetsAuthorProof(tweetIds: Seq[Long]) { - def toThrift: t.TweetsAuthorProof = { - t.TweetsAuthorProof(tweetIds) - } - - def toOfflineThrift: offline.TweetsAuthorProof = - offline.TweetsAuthorProof(tweetIds) -} - -object TweetsAuthorProof { - def fromThrift(proof: t.TweetsAuthorProof): TweetsAuthorProof = { - TweetsAuthorProof(proof.tweetIds) - } -} - -case class DeviceFollowProof(isDeviceFollow: Boolean) { - def toThrift: t.DeviceFollowProof = { - t.DeviceFollowProof(isDeviceFollow) - } - def toOfflineThrift: offline.DeviceFollowProof = - offline.DeviceFollowProof(isDeviceFollow) -} - -object DeviceFollowProof { - def fromThrift(proof: t.DeviceFollowProof): DeviceFollowProof = { - DeviceFollowProof(proof.isDeviceFollow) - } - -} - -case class AccountProof( - followProof: Option[FollowProof] = None, - similarToProof: Option[SimilarToProof] = None, - popularInGeoProof: Option[PopularInGeoProof] = None, - tttInterestProof: Option[TttInterestProof] = None, - topicProof: Option[TopicProof] = None, - customInterestProof: Option[CustomInterest] = None, - tweetsAuthorProof: Option[TweetsAuthorProof] = None, - deviceFollowProof: Option[DeviceFollowProof] = None) { - def toThrift: t.AccountProof = { - t.AccountProof( - followProof.map(_.toThrift), - similarToProof.map(_.toThrift), - popularInGeoProof.map(_.toThrift), - tttInterestProof.map(_.toThrift), - topicProof.map(_.toThrift), - customInterestProof.map(_.toThrift), - tweetsAuthorProof.map(_.toThrift), - deviceFollowProof.map(_.toThrift) - ) - } - - def toOfflineThrift: offline.AccountProof = { - offline.AccountProof( - followProof.map(_.toOfflineThrift), - similarToProof.map(_.toOfflineThrift), - popularInGeoProof.map(_.toOfflineThrift), - tttInterestProof.map(_.toOfflineThrift), - topicProof.map(_.toOfflineThrift), - customInterestProof.map(_.toOfflineThrift), - tweetsAuthorProof.map(_.toOfflineThrift), - deviceFollowProof.map(_.toOfflineThrift) - ) - } -} - -object AccountProof { - def fromThrift(proof: t.AccountProof): AccountProof = { - AccountProof( - proof.followProof.map(FollowProof.fromThrift), - proof.similarToProof.map(SimilarToProof.fromThrift), - proof.popularInGeoProof.map(PopularInGeoProof.fromThrift), - proof.tttInterestProof.map(TttInterestProof.fromThrift), - proof.topicProof.map(TopicProof.fromThrift), - proof.customInterestProof.map(CustomInterest.fromThrift), - proof.tweetsAuthorProof.map(TweetsAuthorProof.fromThrift), - proof.deviceFollowProof.map(DeviceFollowProof.fromThrift) - ) - } -} - -case class Reason(accountProof: Option[AccountProof]) { - def toThrift: t.Reason = { - t.Reason(accountProof.map(_.toThrift)) - } - - def toOfflineThrift: offline.Reason = { - offline.Reason(accountProof.map(_.toOfflineThrift)) - } -} - -object Reason { - - def fromThrift(reason: t.Reason): Reason = { - Reason(reason.accountProof.map(AccountProof.fromThrift)) - } -} - -trait HasReason { - - def reason: Option[Reason] - // helper methods below - - def followedBy: Option[Seq[Long]] = { - for { - reason <- reason - accountProof <- reason.accountProof - followProof <- accountProof.followProof - } yield { followProof.followedBy } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RecentlyEngagedUserId.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RecentlyEngagedUserId.scala deleted file mode 100644 index b20d5d6fb..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RecentlyEngagedUserId.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.{thriftscala => t} - -case class RecentlyEngagedUserId(id: Long, engagementType: EngagementType) { - def toThrift: t.RecentlyEngagedUserId = - t.RecentlyEngagedUserId(id = id, engagementType = engagementType.toThrift) - - def toOfflineThrift: offline.RecentlyEngagedUserId = - offline.RecentlyEngagedUserId(id = id, engagementType = engagementType.toOfflineThrift) -} - -object RecentlyEngagedUserId { - def fromThrift(recentlyEngagedUserId: t.RecentlyEngagedUserId): RecentlyEngagedUserId = { - RecentlyEngagedUserId( - id = recentlyEngagedUserId.id, - engagementType = EngagementType.fromThrift(recentlyEngagedUserId.engagementType) - ) - } - - def fromOfflineThrift( - recentlyEngagedUserId: offline.RecentlyEngagedUserId - ): RecentlyEngagedUserId = { - RecentlyEngagedUserId( - id = recentlyEngagedUserId.id, - engagementType = EngagementType.fromOfflineThrift(recentlyEngagedUserId.engagementType) - ) - } - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RecommendationStep.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RecommendationStep.scala deleted file mode 100644 index 4fd4bc70e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/RecommendationStep.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.{thriftscala => t} -import com.twitter.follow_recommendations.logging.{thriftscala => offline} - -case class RecommendationStep( - recommendations: Seq[FlowRecommendation], - followedUserIds: Set[Long]) { - - def toThrift: t.RecommendationStep = t.RecommendationStep( - recommendations = recommendations.map(_.toThrift), - followedUserIds = followedUserIds - ) - - def toOfflineThrift: offline.OfflineRecommendationStep = - offline.OfflineRecommendationStep( - recommendations = recommendations.map(_.toOfflineThrift), - followedUserIds = followedUserIds) - -} - -object RecommendationStep { - - def fromThrift(recommendationStep: t.RecommendationStep): RecommendationStep = { - RecommendationStep( - recommendations = recommendationStep.recommendations.map(FlowRecommendation.fromThrift), - followedUserIds = recommendationStep.followedUserIds.toSet) - } - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/STPGraph.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/STPGraph.scala deleted file mode 100644 index 2d577e21d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/STPGraph.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.hermit.model.Algorithm.Algorithm -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.FirstDegreeEdge -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.FirstDegreeEdgeInfo -import com.twitter.wtf.scalding.jobs.strong_tie_prediction.SecondDegreeEdge - -case class PotentialFirstDegreeEdge( - userId: Long, - connectingId: Long, - algorithm: Algorithm, - score: Double, - edgeInfo: FirstDegreeEdgeInfo) - -case class IntermediateSecondDegreeEdge( - connectingId: Long, - candidateId: Long, - edgeInfo: FirstDegreeEdgeInfo) - -case class STPGraph( - firstDegreeEdgeInfoList: List[FirstDegreeEdge], - secondDegreeEdgeInfoList: List[SecondDegreeEdge]) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/SafetyLevel.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/SafetyLevel.scala deleted file mode 100644 index 10c21704a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/SafetyLevel.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.spam.rtf.thriftscala.{SafetyLevel => ThriftSafetyLevel} - -sealed trait SafetyLevel { - def toThrift: ThriftSafetyLevel -} - -object SafetyLevel { - case object Recommendations extends SafetyLevel { - override val toThrift = ThriftSafetyLevel.Recommendations - } - - case object TopicsLandingPageTopicRecommendations extends SafetyLevel { - override val toThrift = ThriftSafetyLevel.TopicsLandingPageTopicRecommendations - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Score.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Score.scala deleted file mode 100644 index 1007c5827..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Score.scala +++ /dev/null @@ -1,144 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.common.rankers.common.RankerId -import com.twitter.follow_recommendations.common.rankers.common.RankerId.RankerId -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.{thriftscala => t} - -/** - * Type of Score. This is used to differentiate scores. - * - * Define it as a trait so it is possible to add more information for different score types. - */ -sealed trait ScoreType { - def getName: String -} - -/** - * Existing Score Types - */ -object ScoreType { - - /** - * the score is calculated based on heuristics and most likely not normalized - */ - case object HeuristicBasedScore extends ScoreType { - override def getName: String = "HeuristicBasedScore" - } - - /** - * probability of follow after the candidate is recommended to the user - */ - case object PFollowGivenReco extends ScoreType { - override def getName: String = "PFollowGivenReco" - } - - /** - * probability of engage after the user follows the candidate - */ - case object PEngagementGivenFollow extends ScoreType { - override def getName: String = "PEngagementGivenFollow" - } - - /** - * probability of engage per tweet impression - */ - case object PEngagementPerImpression extends ScoreType { - override def getName: String = "PEngagementPerImpression" - } - - /** - * probability of engage per tweet impression - */ - case object PEngagementGivenReco extends ScoreType { - override def getName: String = "PEngagementGivenReco" - } - - def fromScoreTypeString(scoreTypeName: String): ScoreType = scoreTypeName match { - case "HeuristicBasedScore" => HeuristicBasedScore - case "PFollowGivenReco" => PFollowGivenReco - case "PEngagementGivenFollow" => PEngagementGivenFollow - case "PEngagementPerImpression" => PEngagementPerImpression - case "PEngagementGivenReco" => PEngagementGivenReco - } -} - -/** - * Represent the output from a certain ranker or scorer. All the fields are optional - * - * @param value value of the score - * @param rankerId ranker id - * @param scoreType score type - */ -final case class Score( - value: Double, - rankerId: Option[RankerId] = None, - scoreType: Option[ScoreType] = None) { - - def toThrift: t.Score = t.Score( - value = value, - rankerId = rankerId.map(_.toString), - scoreType = scoreType.map(_.getName) - ) - - def toOfflineThrift: offline.Score = - offline.Score( - value = value, - rankerId = rankerId.map(_.toString), - scoreType = scoreType.map(_.getName) - ) -} - -object Score { - - val RandomScore = Score(0.0d, Some(RankerId.RandomRanker)) - - def optimusScore(score: Double, scoreType: ScoreType): Score = { - Score(value = score, scoreType = Some(scoreType)) - } - - def predictionScore(score: Double, rankerId: RankerId): Score = { - Score(value = score, rankerId = Some(rankerId)) - } - - def fromThrift(thriftScore: t.Score): Score = - Score( - value = thriftScore.value, - rankerId = thriftScore.rankerId.flatMap(RankerId.getRankerByName), - scoreType = thriftScore.scoreType.map(ScoreType.fromScoreTypeString) - ) -} - -/** - * a list of scores - */ -final case class Scores( - scores: Seq[Score], - selectedRankerId: Option[RankerId] = None, - isInProducerScoringExperiment: Boolean = false) { - - def toThrift: t.Scores = - t.Scores( - scores = scores.map(_.toThrift), - selectedRankerId = selectedRankerId.map(_.toString), - isInProducerScoringExperiment = isInProducerScoringExperiment - ) - - def toOfflineThrift: offline.Scores = - offline.Scores( - scores = scores.map(_.toOfflineThrift), - selectedRankerId = selectedRankerId.map(_.toString), - isInProducerScoringExperiment = isInProducerScoringExperiment - ) -} - -object Scores { - val Empty: Scores = Scores(Nil) - - def fromThrift(thriftScores: t.Scores): Scores = - Scores( - scores = thriftScores.scores.map(Score.fromThrift), - selectedRankerId = thriftScores.selectedRankerId.flatMap(RankerId.getRankerByName), - isInProducerScoringExperiment = thriftScores.isInProducerScoringExperiment - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Session.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Session.scala deleted file mode 100644 index 25ff00b48..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/Session.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.finagle.tracing.Trace - -object Session { - - /** - * The sessionId in FRS is the finagle trace id which is static within the lifetime of a single - * request. - * - * It is used when generating per-candidate tokens (in TrackingTokenTransform) and is also passed - * in to downstream Optimus ranker requests. - * - */ - def getSessionId: Long = Trace.id.traceId.toLong -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/SignalData.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/SignalData.scala deleted file mode 100644 index 3af5c6e1d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/SignalData.scala +++ /dev/null @@ -1,42 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.simclusters_v2.thriftscala.InternalId -import com.twitter.usersignalservice.thriftscala.SignalType -import com.twitter.usersignalservice.thriftscala.Signal - -trait SignalData { - val userId: Long - val signalType: SignalType -} - -case class RecentFollowsSignal( - override val userId: Long, - override val signalType: SignalType, - followedUserId: Long, - timestamp: Long) - extends SignalData - -object RecentFollowsSignal { - - def fromUssSignal(targetUserId: Long, signal: Signal): RecentFollowsSignal = { - val InternalId.UserId(followedUserId) = signal.targetInternalId.getOrElse( - throw new IllegalArgumentException("RecentFollow Signal does not have internalId")) - - RecentFollowsSignal( - userId = targetUserId, - followedUserId = followedUserId, - timestamp = signal.timestamp, - signalType = signal.signalType - ) - } - - def getRecentFollowedUserIds( - signalDataMap: Option[Map[SignalType, Seq[SignalData]]] - ): Option[Seq[Long]] = { - signalDataMap.map(_.getOrElse(SignalType.AccountFollow, default = Seq.empty).flatMap { - case RecentFollowsSignal(userId, signalType, followedUserId, timestamp) => - Some(followedUserId) - case _ => None - }) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/TrackingToken.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/TrackingToken.scala deleted file mode 100644 index 177e08f65..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/TrackingToken.scala +++ /dev/null @@ -1,62 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.finagle.tracing.Trace -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.{thriftscala => t} -import com.twitter.scrooge.BinaryThriftStructSerializer -import com.twitter.suggests.controller_data.thriftscala.ControllerData -import com.twitter.util.Base64StringEncoder - -/** - * used for attribution per target-candidate pair - * @param sessionId trace-id of the finagle request - * @param controllerData 64-bit encoded binary attributes of our recommendation - * @param algorithmId id for identifying a candidate source. maintained for backwards compatibility - */ -case class TrackingToken( - sessionId: Long, - displayLocation: Option[DisplayLocation], - controllerData: Option[ControllerData], - algorithmId: Option[Int]) { - - def toThrift: t.TrackingToken = { - Trace.id.traceId.toLong - t.TrackingToken( - sessionId = sessionId, - displayLocation = displayLocation.map(_.toThrift), - controllerData = controllerData, - algoId = algorithmId - ) - } - - def toOfflineThrift: offline.TrackingToken = { - offline.TrackingToken( - sessionId = sessionId, - displayLocation = displayLocation.map(_.toOfflineThrift), - controllerData = controllerData, - algoId = algorithmId - ) - } -} - -object TrackingToken { - val binaryThriftSerializer = BinaryThriftStructSerializer[t.TrackingToken](t.TrackingToken) - def serialize(trackingToken: TrackingToken): String = { - Base64StringEncoder.encode(binaryThriftSerializer.toBytes(trackingToken.toThrift)) - } - def deserialize(trackingTokenStr: String): TrackingToken = { - fromThrift(binaryThriftSerializer.fromBytes(Base64StringEncoder.decode(trackingTokenStr))) - } - def fromThrift(token: t.TrackingToken): TrackingToken = { - TrackingToken( - sessionId = token.sessionId, - displayLocation = token.displayLocation.map(DisplayLocation.fromThrift), - controllerData = token.controllerData, - algorithmId = token.algoId - ) - } -} - -trait HasTrackingToken { - def trackingToken: Option[TrackingToken] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/TweetCandidate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/TweetCandidate.scala deleted file mode 100644 index 1957e28cc..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/TweetCandidate.scala +++ /dev/null @@ -1,6 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -case class TweetCandidate( - tweetId: Long, - authorId: Long, - score: Option[Double]) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/UserCandidateSourceDetails.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/UserCandidateSourceDetails.scala deleted file mode 100644 index 73b766232..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/UserCandidateSourceDetails.scala +++ /dev/null @@ -1,97 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.{thriftscala => t} -import com.twitter.hermit.constants.AlgorithmFeedbackTokens._ -import com.twitter.hermit.ml.models.Feature -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier - -/** - * primaryCandidateSource param is showing the candidate source that responsible for generating this - * candidate, as the candidate might have gone through multiple candidate sources to get generated - * (for example if it has generated by a composite source). WeightedCandidateSourceRanker uses this - * field to do the sampling over candidate sources. All the sources used for generating this - * candidate (including the primary source) and their corresponding score exist in the - * candidateSourceScores field. - */ -case class UserCandidateSourceDetails( - primaryCandidateSource: Option[CandidateSourceIdentifier], - candidateSourceScores: Map[CandidateSourceIdentifier, Option[Double]] = Map.empty, - candidateSourceRanks: Map[CandidateSourceIdentifier, Int] = Map.empty, - addressBookMetadata: Option[AddressBookMetadata] = None, - candidateSourceFeatures: Map[CandidateSourceIdentifier, Seq[Feature]] = Map.empty, -) { - - def toThrift: t.CandidateSourceDetails = { - t.CandidateSourceDetails( - candidateSourceScores = Some(candidateSourceScores.map { - case (identifier, score) => - (identifier.name, score.getOrElse(0.0d)) - }), - primarySource = for { - identifier <- primaryCandidateSource - algo <- Algorithm.withNameOpt(identifier.name) - feedbackToken <- AlgorithmToFeedbackTokenMap.get(algo) - } yield feedbackToken - ) - } - - def toOfflineThrift: offline.CandidateSourceDetails = { - offline.CandidateSourceDetails( - candidateSourceScores = Some(candidateSourceScores.map { - case (identifier, score) => - (identifier.name, score.getOrElse(0.0d)) - }), - primarySource = for { - identifier <- primaryCandidateSource - algo <- Algorithm.withNameOpt(identifier.name) - feedbackToken <- AlgorithmToFeedbackTokenMap.get(algo) - } yield feedbackToken - ) - } -} - -object UserCandidateSourceDetails { - val algorithmNameMap: Map[String, Algorithm.Value] = Algorithm.values.map { - algorithmValue: Algorithm.Value => - (algorithmValue.toString, algorithmValue) - }.toMap - - /** - * This method is used to parse the candidate source of the candidates, which is only passed from - * the scoreUserCandidates endpoint. We create custom candidate source identifiers which - * CandidateAlgorithmSource will read from to hydrate the algorithm id feature. - * candidateSourceScores will not be populated from the endpoint, but we add the conversion for - * completeness. Note that the conversion uses the raw string of the Algorithm rather than the - * assigned strings that we give to our own candidate sources in the FRS. - */ - def fromThrift(details: t.CandidateSourceDetails): UserCandidateSourceDetails = { - val primaryCandidateSource: Option[CandidateSourceIdentifier] = for { - primarySourceToken <- details.primarySource - algo <- TokenToAlgorithmMap.get(primarySourceToken) - } yield CandidateSourceIdentifier(algo.toString) - - val candidateSourceScores = for { - scoreMap <- details.candidateSourceScores.toSeq - (name, score) <- scoreMap - algo <- algorithmNameMap.get(name) - } yield { - CandidateSourceIdentifier(algo.toString) -> Some(score) - } - val candidateSourceRanks = for { - rankMap <- details.candidateSourceRanks.toSeq - (name, rank) <- rankMap - algo <- algorithmNameMap.get(name) - } yield { - CandidateSourceIdentifier(algo.toString) -> rank - } - UserCandidateSourceDetails( - primaryCandidateSource = primaryCandidateSource, - candidateSourceScores = candidateSourceScores.toMap, - candidateSourceRanks = candidateSourceRanks.toMap, - addressBookMetadata = None, - candidateSourceFeatures = Map.empty - ) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/UserIdAndTimestamp.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/UserIdAndTimestamp.scala deleted file mode 100644 index 74f33eb40..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/UserIdAndTimestamp.scala +++ /dev/null @@ -1,3 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -case class UserIdWithTimestamp(userId: Long, timeInMs: Long) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/WtfImpression.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/WtfImpression.scala deleted file mode 100644 index 39e0561a2..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/WtfImpression.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.common.models - -import com.twitter.util.Time - -/** - * Domain model for representing impressions on wtf recommendations in the past 16 days - */ -case class WtfImpression( - candidateId: Long, - displayLocation: DisplayLocation, - latestTime: Time, - counts: Int) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/BUILD deleted file mode 100644 index ffcbe65a7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/BUILD +++ /dev/null @@ -1,21 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "escherbird/src/scala/com/twitter/escherbird/util/stitchcache", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/user_state", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/features", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/stores", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CandidateParamPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CandidateParamPredicate.scala deleted file mode 100644 index 0713f728f..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CandidateParamPredicate.scala +++ /dev/null @@ -1,21 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates - -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.models.FilterReason -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Param - -class CandidateParamPredicate[A <: HasParams]( - param: Param[Boolean], - reason: FilterReason) - extends Predicate[A] { - override def apply(candidate: A): Stitch[PredicateResult] = { - if (candidate.params(param)) { - Stitch.value(PredicateResult.Valid) - } else { - Stitch.value(PredicateResult.Invalid(Set(reason))) - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CandidateSourceParamPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CandidateSourceParamPredicate.scala deleted file mode 100644 index cf08f5623..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CandidateSourceParamPredicate.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates - -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FilterReason -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.Param - -/** - * This predicate allows us to filter candidates given its source. - * To avoid bucket dilution, we only want to evaluate the param (which would implicitly trigger - * bucketing for FSParams) only if the candidate source fn yields true. - * The param provided should be true when we want to keep the candidate and false otherwise. - */ -class CandidateSourceParamPredicate( - val param: Param[Boolean], - val reason: FilterReason, - candidateSources: Set[CandidateSourceIdentifier]) - extends Predicate[CandidateUser] { - override def apply(candidate: CandidateUser): Stitch[PredicateResult] = { - // we want to avoid evaluating the param if the candidate source fn yields false - if (candidate.getCandidateSources.keys.exists(candidateSources.contains) && !candidate.params( - param)) { - Stitch.value(PredicateResult.Invalid(Set(reason))) - } else { - Stitch.value(PredicateResult.Valid) - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CuratedCompetitorListPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CuratedCompetitorListPredicate.scala deleted file mode 100644 index 16d76ce44..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/CuratedCompetitorListPredicate.scala +++ /dev/null @@ -1,66 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates - -import com.google.inject.name.Named -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.follow_recommendations.common.models.FilterReason.CuratedAccountsCompetitorList -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.stitch.Stitch -import com.twitter.strato.client.Fetcher -import javax.inject.Inject -import javax.inject.Singleton -import com.twitter.conversions.DurationOps._ -import com.twitter.escherbird.util.stitchcache.StitchCache - -@Singleton -case class CuratedCompetitorListPredicate @Inject() ( - statsReceiver: StatsReceiver, - @Named(GuiceNamedConstants.CURATED_COMPETITOR_ACCOUNTS_FETCHER) competitorAccountFetcher: Fetcher[ - String, - Unit, - Seq[Long] - ]) extends Predicate[CandidateUser] { - - private val stats: StatsReceiver = statsReceiver.scope(this.getClass.getName) - private val cacheStats = stats.scope("cache") - - private val cache = StitchCache[String, Set[Long]]( - maxCacheSize = CuratedCompetitorListPredicate.CacheNumberOfEntries, - ttl = CuratedCompetitorListPredicate.CacheTTL, - statsReceiver = cacheStats, - underlyingCall = (competitorListPrefix: String) => query(competitorListPrefix) - ) - - private def query(prefix: String): Stitch[Set[Long]] = - competitorAccountFetcher.fetch(prefix).map(_.v.getOrElse(Nil).toSet) - - /** - * Caveat here is that though the similarToUserIds allows for a Seq[Long], in practice we would - * only return 1 userId. Multiple userId's would result in filtering candidates associated with - * a different similarToUserId. For example: - * - similarToUser1 -> candidate1, candidate2 - * - similarToUser2 -> candidate3 - * and in the competitorList store we have: - * - similarToUser1 -> candidate3 - * we'll be filtering candidate3 on account of similarToUser1, even though it was generated - * with similarToUser2. This might still be desirable at a product level (since we don't want - * to show these accounts anyway), but might not achieve what you intend to code-wise. - */ - override def apply(candidate: CandidateUser): Stitch[PredicateResult] = { - cache.readThrough(CuratedCompetitorListPredicate.DefaultKey).map { competitorListAccounts => - if (competitorListAccounts.contains(candidate.id)) { - PredicateResult.Invalid(Set(CuratedAccountsCompetitorList)) - } else { - PredicateResult.Valid - } - } - } -} - -object CuratedCompetitorListPredicate { - val DefaultKey: String = "default_list" - val CacheTTL = 5.minutes - val CacheNumberOfEntries = 5 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/ExcludedUserIdPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/ExcludedUserIdPredicate.scala deleted file mode 100644 index 01a5f96fd..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/ExcludedUserIdPredicate.scala +++ /dev/null @@ -1,24 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates - -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.models.FilterReason.ExcludedId -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasExcludedUserIds -import com.twitter.stitch.Stitch - -object ExcludedUserIdPredicate extends Predicate[(HasExcludedUserIds, CandidateUser)] { - - val ValidStitch: Stitch[PredicateResult.Valid.type] = Stitch.value(PredicateResult.Valid) - val ExcludedStitch: Stitch[PredicateResult.Invalid] = - Stitch.value(PredicateResult.Invalid(Set(ExcludedId))) - - override def apply(pair: (HasExcludedUserIds, CandidateUser)): Stitch[PredicateResult] = { - val (excludedUserIds, candidate) = pair - if (excludedUserIds.excludedUserIds.contains(candidate.id)) { - ExcludedStitch - } else { - ValidStitch - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/InactivePredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/InactivePredicate.scala deleted file mode 100644 index c77538a99..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/InactivePredicate.scala +++ /dev/null @@ -1,121 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates - -import com.google.inject.name.Named -import com.twitter.core_workflows.user_model.thriftscala.UserState -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FilterReason -import com.twitter.follow_recommendations.common.predicates.InactivePredicateParams._ -import com.twitter.service.metastore.gen.thriftscala.UserRecommendabilityFeatures -import com.twitter.stitch.Stitch -import com.twitter.strato.client.Fetcher -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.Duration -import com.twitter.util.Time -import javax.inject.Inject -import javax.inject.Singleton -import com.twitter.conversions.DurationOps._ -import com.twitter.escherbird.util.stitchcache.StitchCache -import com.twitter.follow_recommendations.common.models.HasUserState -import com.twitter.follow_recommendations.common.predicates.InactivePredicateParams.DefaultInactivityThreshold -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext - -import java.lang.{Long => JLong} - -@Singleton -case class InactivePredicate @Inject() ( - statsReceiver: StatsReceiver, - @Named(GuiceNamedConstants.USER_RECOMMENDABILITY_FETCHER) userRecommendabilityFetcher: Fetcher[ - Long, - Unit, - UserRecommendabilityFeatures - ]) extends Predicate[(HasParams with HasClientContext with HasUserState, CandidateUser)] { - - private val stats: StatsReceiver = statsReceiver.scope("InactivePredicate") - private val cacheStats = stats.scope("cache") - - private def queryUserRecommendable(userId: Long): Stitch[Option[UserRecommendabilityFeatures]] = - userRecommendabilityFetcher.fetch(userId).map(_.v) - - private val userRecommendableCache = - StitchCache[JLong, Option[UserRecommendabilityFeatures]]( - maxCacheSize = 100000, - ttl = 12.hours, - statsReceiver = cacheStats.scope("UserRecommendable"), - underlyingCall = (userId: JLong) => queryUserRecommendable(userId) - ) - - override def apply( - targetAndCandidate: (HasParams with HasClientContext with HasUserState, CandidateUser) - ): Stitch[PredicateResult] = { - val (target, candidate) = targetAndCandidate - - userRecommendableCache - .readThrough(candidate.id).map { - case recFeaturesFetchResult => - recFeaturesFetchResult match { - case None => - PredicateResult.Invalid(Set(FilterReason.MissingRecommendabilityData)) - case Some(recFeatures) => - if (disableInactivityPredicate(target, target.userState, recFeatures.userState)) { - PredicateResult.Valid - } else { - val defaultInactivityThreshold = target.params(DefaultInactivityThreshold).days - val hasBeenActiveRecently = recFeatures.lastStatusUpdateMs - .map(Time.now - Time.fromMilliseconds(_)).getOrElse( - Duration.Top) < defaultInactivityThreshold - stats - .scope(defaultInactivityThreshold.toString).counter( - if (hasBeenActiveRecently) - "active" - else - "inactive" - ).incr() - if (hasBeenActiveRecently && (!target - .params(UseEggFilter) || recFeatures.isNotEgg.contains(1))) { - PredicateResult.Valid - } else { - PredicateResult.Invalid(Set(FilterReason.Inactive)) - } - } - } - }.rescue { - case e: Exception => - stats.counter(e.getClass.getSimpleName).incr() - Stitch(PredicateResult.Invalid(Set(FilterReason.FailOpen))) - } - } - - private[this] def disableInactivityPredicate( - target: HasParams, - consumerState: Option[UserState], - candidateState: Option[UserState] - ): Boolean = { - target.params(MightBeDisabled) && - consumerState.exists(InactivePredicate.ValidConsumerStates.contains) && - ( - ( - candidateState.exists(InactivePredicate.ValidCandidateStates.contains) && - !target.params(OnlyDisableForNewUserStateCandidates) - ) || - ( - candidateState.contains(UserState.New) && - target.params(OnlyDisableForNewUserStateCandidates) - ) - ) - } -} - -object InactivePredicate { - val ValidConsumerStates: Set[UserState] = Set( - UserState.HeavyNonTweeter, - UserState.MediumNonTweeter, - UserState.HeavyTweeter, - UserState.MediumTweeter - ) - val ValidCandidateStates: Set[UserState] = - Set(UserState.New, UserState.VeryLight, UserState.Light, UserState.NearZero) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/InactivePredicateParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/InactivePredicateParams.scala deleted file mode 100644 index 0bb52caa8..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/InactivePredicateParams.scala +++ /dev/null @@ -1,21 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.Param - -object InactivePredicateParams { - case object DefaultInactivityThreshold - extends FSBoundedParam[Int]( - name = "inactive_predicate_default_inactivity_threshold", - default = 60, - min = 1, - max = 500 - ) - case object UseEggFilter extends Param[Boolean](true) - case object MightBeDisabled extends FSParam[Boolean]("inactive_predicate_might_be_disabled", true) - case object OnlyDisableForNewUserStateCandidates - extends FSParam[Boolean]( - "inactive_predicate_only_disable_for_new_user_state_candidates", - false) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/PreviouslyRecommendedUserIdsPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/PreviouslyRecommendedUserIdsPredicate.scala deleted file mode 100644 index 7879860ac..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/PreviouslyRecommendedUserIdsPredicate.scala +++ /dev/null @@ -1,34 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates - -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FilterReason -import com.twitter.follow_recommendations.common.models.HasPreviousRecommendationsContext -import com.twitter.stitch.Stitch -import javax.inject.Singleton - -@Singleton -class PreviouslyRecommendedUserIdsPredicate - extends Predicate[(HasPreviousRecommendationsContext, CandidateUser)] { - override def apply( - pair: (HasPreviousRecommendationsContext, CandidateUser) - ): Stitch[PredicateResult] = { - - val (targetUser, candidate) = pair - - val previouslyRecommendedUserIDs = targetUser.previouslyRecommendedUserIDs - - if (!previouslyRecommendedUserIDs.contains(candidate.id)) { - PreviouslyRecommendedUserIdsPredicate.ValidStitch - } else { - PreviouslyRecommendedUserIdsPredicate.AlreadyRecommendedStitch - } - } -} - -object PreviouslyRecommendedUserIdsPredicate { - val ValidStitch: Stitch[PredicateResult.Valid.type] = Stitch.value(PredicateResult.Valid) - val AlreadyRecommendedStitch: Stitch[PredicateResult.Invalid] = - Stitch.value(PredicateResult.Invalid(Set(FilterReason.AlreadyRecommended))) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/BUILD deleted file mode 100644 index 9d1ca9b40..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/BUILD +++ /dev/null @@ -1,17 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/dismiss_store", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/DismissedCandidatePredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/DismissedCandidatePredicate.scala deleted file mode 100644 index 550017b95..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/DismissedCandidatePredicate.scala +++ /dev/null @@ -1,32 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.dismiss - -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.models.FilterReason.DismissedId -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDismissedUserIds -import com.twitter.stitch.Stitch -import javax.inject.Singleton - -@Singleton -class DismissedCandidatePredicate extends Predicate[(HasDismissedUserIds, CandidateUser)] { - - override def apply(pair: (HasDismissedUserIds, CandidateUser)): Stitch[PredicateResult] = { - - val (targetUser, candidate) = pair - targetUser.dismissedUserIds - .map { dismissedUserIds => - if (!dismissedUserIds.contains(candidate.id)) { - DismissedCandidatePredicate.ValidStitch - } else { - DismissedCandidatePredicate.DismissedStitch - } - }.getOrElse(DismissedCandidatePredicate.ValidStitch) - } -} - -object DismissedCandidatePredicate { - val ValidStitch: Stitch[PredicateResult.Valid.type] = Stitch.value(PredicateResult.Valid) - val DismissedStitch: Stitch[PredicateResult.Invalid] = - Stitch.value(PredicateResult.Invalid(Set(DismissedId))) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/DismissedCandidatePredicateParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/DismissedCandidatePredicateParams.scala deleted file mode 100644 index 7f1d51a07..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss/DismissedCandidatePredicateParams.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.dismiss - -import com.twitter.conversions.DurationOps._ -import com.twitter.timelines.configapi.Param -import com.twitter.util.Duration - -object DismissedCandidatePredicateParams { - case object LookBackDuration extends Param[Duration](180.days) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/BUILD deleted file mode 100644 index a154121e6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/BUILD +++ /dev/null @@ -1,23 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "escherbird/src/scala/com/twitter/escherbird/util/stitchcache", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders", - "stitch/stitch-gizmoduck", - "util/util-slf4j-api/src/main/scala", - "util/util-thrift", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicate.scala deleted file mode 100644 index 2ca3e2fc5..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicate.scala +++ /dev/null @@ -1,284 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.gizmoduck - -import com.twitter.decider.Decider -import com.twitter.decider.RandomRecipient -import com.twitter.escherbird.util.stitchcache.StitchCache -import com.twitter.finagle.Memcached.Client -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.finagle.util.DefaultTimer -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.clients.cache.MemcacheClient -import com.twitter.follow_recommendations.common.clients.cache.ThriftBijection -import com.twitter.follow_recommendations.common.models.FilterReason._ -import com.twitter.follow_recommendations.common.models.AddressBookMetadata -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FilterReason -import com.twitter.follow_recommendations.common.predicates.gizmoduck.GizmoduckPredicate._ -import com.twitter.follow_recommendations.common.predicates.gizmoduck.GizmoduckPredicateParams._ -import com.twitter.follow_recommendations.configapi.deciders.DeciderKey -import com.twitter.gizmoduck.thriftscala.LabelValue.BlinkBad -import com.twitter.gizmoduck.thriftscala.LabelValue.BlinkWorst -import com.twitter.gizmoduck.thriftscala.LabelValue -import com.twitter.gizmoduck.thriftscala.LookupContext -import com.twitter.gizmoduck.thriftscala.QueryFields -import com.twitter.gizmoduck.thriftscala.User -import com.twitter.gizmoduck.thriftscala.UserResult -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.scrooge.CompactThriftSerializer -import com.twitter.spam.rtf.thriftscala.SafetyLevel -import com.twitter.stitch.Stitch -import com.twitter.stitch.gizmoduck.Gizmoduck -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.Duration -import com.twitter.util.logging.Logging -import java.lang.{Long => JLong} -import javax.inject.Inject -import javax.inject.Singleton - -/** - * In this filter, we want to check 4 categories of conditions: - * - if candidate is discoverable given that it's from an address-book/phone-book based source - * - if candidate is unsuitable based on it's safety sub-fields in gizmoduck - * - if candidate is withheld because of country-specific take-down policies - * - if candidate is marked as bad/worst based on blink labels - * We fail close on the query as this is a product-critical filter - */ -@Singleton -case class GizmoduckPredicate @Inject() ( - gizmoduck: Gizmoduck, - client: Client, - statsReceiver: StatsReceiver, - decider: Decider = Decider.False) - extends Predicate[(HasClientContext with HasParams, CandidateUser)] - with Logging { - private val stats: StatsReceiver = statsReceiver.scope(this.getClass.getName) - - // track # of Gizmoduck predicate queries that yielded valid & invalid predicate results - private val validPredicateResultCounter = stats.counter("predicate_valid") - private val invalidPredicateResultCounter = stats.counter("predicate_invalid") - - // track # of cases where no Gizmoduck user was found - private val noGizmoduckUserCounter = stats.counter("no_gizmoduck_user_found") - - private val gizmoduckCache = StitchCache[JLong, UserResult]( - maxCacheSize = MaxCacheSize, - ttl = CacheTTL, - statsReceiver = stats.scope("cache"), - underlyingCall = getByUserId - ) - - // Distributed Twemcache to store UserResult objects keyed on user IDs - val bijection = new ThriftBijection[UserResult] { - override val serializer = CompactThriftSerializer(UserResult) - } - val memcacheClient = MemcacheClient[UserResult]( - client = client, - dest = "/s/cache/frs:twemcaches", - valueBijection = bijection, - ttl = CacheTTL, - statsReceiver = stats.scope("twemcache") - ) - - // main method used to apply GizmoduckPredicate to a candidate user - override def apply( - pair: (HasClientContext with HasParams, CandidateUser) - ): Stitch[PredicateResult] = { - val (request, candidate) = pair - // measure the latency of the getGizmoduckPredicateResult, since this predicate - // check is product-critical and relies on querying a core service (Gizmoduck) - StatsUtil.profileStitch( - getGizmoduckPredicateResult(request, candidate), - stats.scope("getGizmoduckPredicateResult") - ) - } - - private def getGizmoduckPredicateResult( - request: HasClientContext with HasParams, - candidate: CandidateUser - ): Stitch[PredicateResult] = { - val timeout: Duration = request.params(GizmoduckGetTimeout) - - val deciderKey: String = DeciderKey.EnableGizmoduckCaching.toString - val enableDistributedCaching: Boolean = decider.isAvailable(deciderKey, Some(RandomRecipient)) - - // try getting an existing UserResult from cache if possible - val userResultStitch: Stitch[UserResult] = - enableDistributedCaching match { - // read from memcache - case true => memcacheClient.readThrough( - // add a key prefix to address cache key collisions - key = "GizmoduckPredicate" + candidate.id.toString, - underlyingCall = () => getByUserId(candidate.id) - ) - // read from local cache - case false => gizmoduckCache.readThrough(candidate.id) - } - - val predicateResultStitch = userResultStitch.map { - userResult => { - val predicateResult = getPredicateResult(request, candidate, userResult) - if (enableDistributedCaching) { - predicateResult match { - case PredicateResult.Valid => - stats.scope("twemcache").counter("predicate_valid").incr() - case PredicateResult.Invalid(reasons) => - stats.scope("twemcache").counter("predicate_invalid").incr() - } - // log metrics to check if local cache value matches distributed cache value - logPredicateResultEquality( - request, - candidate, - predicateResult - ) - } else { - predicateResult match { - case PredicateResult.Valid => - stats.scope("cache").counter("predicate_valid").incr() - case PredicateResult.Invalid(reasons) => - stats.scope("cache").counter("predicate_invalid").incr() - } - } - predicateResult - } - } - predicateResultStitch - .within(timeout)(DefaultTimer) - .rescue { // fail-open when timeout or exception - case e: Exception => - stats.scope("rescued").counter(e.getClass.getSimpleName).incr() - invalidPredicateResultCounter.incr() - Stitch(PredicateResult.Invalid(Set(FailOpen))) - } - } - - private def logPredicateResultEquality( - request: HasClientContext with HasParams, - candidate: CandidateUser, - predicateResult: PredicateResult - ): Unit = { - val localCachedUserResult = Option(gizmoduckCache.cache.getIfPresent(candidate.id)) - if (localCachedUserResult.isDefined) { - val localPredicateResult = getPredicateResult(request, candidate, localCachedUserResult.get) - localPredicateResult.equals(predicateResult) match { - case true => stats.scope("has_equal_predicate_value").counter("true").incr() - case false => stats.scope("has_equal_predicate_value").counter("false").incr() - } - } else { - stats.scope("has_equal_predicate_value").counter("undefined").incr() - } - } - - // method to get PredicateResult from UserResult - def getPredicateResult( - request: HasClientContext with HasParams, - candidate: CandidateUser, - userResult: UserResult, - ): PredicateResult = { - userResult.user match { - case Some(user) => - val abPbReasons = getAbPbReason(user, candidate.getAddressBookMetadata) - val safetyReasons = getSafetyReasons(user) - val countryTakedownReasons = getCountryTakedownReasons(user, request.getCountryCode) - val blinkReasons = getBlinkReasons(user) - val allReasons = - abPbReasons ++ safetyReasons ++ countryTakedownReasons ++ blinkReasons - if (allReasons.nonEmpty) { - invalidPredicateResultCounter.incr() - PredicateResult.Invalid(allReasons) - } else { - validPredicateResultCounter.incr() - PredicateResult.Valid - } - case None => - noGizmoduckUserCounter.incr() - invalidPredicateResultCounter.incr() - PredicateResult.Invalid(Set(NoUser)) - } - } - - private def getByUserId(userId: JLong): Stitch[UserResult] = { - StatsUtil.profileStitch( - gizmoduck.getById(userId = userId, queryFields = queryFields, context = lookupContext), - stats.scope("getByUserId") - ) - } -} - -object GizmoduckPredicate { - - private[gizmoduck] val lookupContext: LookupContext = - LookupContext(`includeDeactivated` = true, `safetyLevel` = Some(SafetyLevel.Recommendations)) - - private[gizmoduck] val queryFields: Set[QueryFields] = - Set( - QueryFields.Discoverability, // needed for Address Book / Phone Book discoverability checks in getAbPbReason - QueryFields.Safety, // needed for user state safety checks in getSafetyReasons, getCountryTakedownReasons - QueryFields.Labels, // needed for user label checks in getBlinkReasons - QueryFields.Takedowns // needed for checking takedown labels for a user in getCountryTakedownReasons - ) - - private[gizmoduck] val BlinkLabels: Set[LabelValue] = Set(BlinkBad, BlinkWorst) - - private[gizmoduck] def getAbPbReason( - user: User, - abMetadataOpt: Option[AddressBookMetadata] - ): Set[FilterReason] = { - (for { - discoverability <- user.discoverability - abMetadata <- abMetadataOpt - } yield { - val AddressBookMetadata(fwdPb, rvPb, fwdAb, rvAb) = abMetadata - val abReason: Set[FilterReason] = - if ((!discoverability.discoverableByEmail) && (fwdAb || rvAb)) - Set(AddressBookUndiscoverable) - else Set.empty - val pbReason: Set[FilterReason] = - if ((!discoverability.discoverableByMobilePhone) && (fwdPb || rvPb)) - Set(PhoneBookUndiscoverable) - else Set.empty - abReason ++ pbReason - }).getOrElse(Set.empty) - } - - private[gizmoduck] def getSafetyReasons(user: User): Set[FilterReason] = { - user.safety - .map { s => - val deactivatedReason: Set[FilterReason] = - if (s.deactivated) Set(Deactivated) else Set.empty - val suspendedReason: Set[FilterReason] = if (s.suspended) Set(Suspended) else Set.empty - val restrictedReason: Set[FilterReason] = if (s.restricted) Set(Restricted) else Set.empty - val nsfwUserReason: Set[FilterReason] = if (s.nsfwUser) Set(NsfwUser) else Set.empty - val nsfwAdminReason: Set[FilterReason] = if (s.nsfwAdmin) Set(NsfwAdmin) else Set.empty - val isProtectedReason: Set[FilterReason] = if (s.isProtected) Set(IsProtected) else Set.empty - deactivatedReason ++ suspendedReason ++ restrictedReason ++ nsfwUserReason ++ nsfwAdminReason ++ isProtectedReason - }.getOrElse(Set.empty) - } - - private[gizmoduck] def getCountryTakedownReasons( - user: User, - countryCodeOpt: Option[String] - ): Set[FilterReason] = { - (for { - safety <- user.safety.toSeq - if safety.hasTakedown - takedowns <- user.takedowns.toSeq - takedownCountry <- takedowns.countryCodes - requestingCountry <- countryCodeOpt - if takedownCountry.toLowerCase == requestingCountry.toLowerCase - } yield Set(CountryTakedown(takedownCountry.toLowerCase))).flatten.toSet - } - - private[gizmoduck] def getBlinkReasons(user: User): Set[FilterReason] = { - user.labels - .map(_.labels.map(_.labelValue)) - .getOrElse(Nil) - .exists(BlinkLabels.contains) - for { - labels <- user.labels.toSeq - label <- labels.labels - if (BlinkLabels.contains(label.labelValue)) - } yield Set(Blink) - }.flatten.toSet -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateCache.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateCache.scala deleted file mode 100644 index 36fb2f20f..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateCache.scala +++ /dev/null @@ -1,50 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.gizmoduck - -import java.util.concurrent.TimeUnit - -import com.google.common.base.Ticker -import com.google.common.cache.CacheBuilder -import com.google.common.cache.Cache -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.util.Time -import com.twitter.util.Duration - -/** - * In-memory cache used for caching GizmoduckPredicate query calls in - * com.twitter.follow_recommendations.common.predicates.gizmoduck.GizmoduckPredicate. - * - * References the cache implementation in com.twitter.escherbird.util.stitchcache, - * but without the underlying Stitch call. - */ -object GizmoduckPredicateCache { - - private[GizmoduckPredicateCache] class TimeTicker extends Ticker { - override def read(): Long = Time.now.inNanoseconds - } - - def apply[K, V]( - maxCacheSize: Int, - ttl: Duration, - statsReceiver: StatsReceiver - ): Cache[K, V] = { - - val cache: Cache[K, V] = - CacheBuilder - .newBuilder() - .maximumSize(maxCacheSize) - .asInstanceOf[CacheBuilder[K, V]] - .expireAfterWrite(ttl.inSeconds, TimeUnit.SECONDS) - .recordStats() - .ticker(new TimeTicker()) - .build() - - // metrics for tracking cache usage - statsReceiver.provideGauge("cache_size") { cache.size.toFloat } - statsReceiver.provideGauge("cache_hits") { cache.stats.hitCount.toFloat } - statsReceiver.provideGauge("cache_misses") { cache.stats.missCount.toFloat } - statsReceiver.provideGauge("cache_hit_rate") { cache.stats.hitRate.toFloat } - statsReceiver.provideGauge("cache_evictions") { cache.stats.evictionCount.toFloat } - - cache - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateFSConfig.scala deleted file mode 100644 index 447eac835..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateFSConfig.scala +++ /dev/null @@ -1,17 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.gizmoduck - -import com.twitter.follow_recommendations.common.predicates.gizmoduck.GizmoduckPredicateParams._ -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.util.Duration - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class GizmoduckPredicateFSConfig @Inject() () extends FeatureSwitchConfig { - override val durationFSParams: Seq[FSBoundedParam[Duration] with HasDurationConversion] = Seq( - GizmoduckGetTimeout - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateParams.scala deleted file mode 100644 index 811897e27..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck/GizmoduckPredicateParams.scala +++ /dev/null @@ -1,21 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.gizmoduck - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.DurationConversion -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.util.Duration -import com.twitter.conversions.DurationOps._ - -object GizmoduckPredicateParams { - case object GizmoduckGetTimeout - extends FSBoundedParam[Duration]( - name = "gizmoduck_predicate_timeout_in_millis", - default = 200.millisecond, - min = 1.millisecond, - max = 500.millisecond) - with HasDurationConversion { - override def durationConversion: DurationConversion = DurationConversion.FromMillis - } - val MaxCacheSize: Int = 250000 - val CacheTTL: Duration = Duration.fromHours(6) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/BUILD deleted file mode 100644 index d0e9e1015..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/BUILD +++ /dev/null @@ -1,21 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "escherbird/src/scala/com/twitter/escherbird/util/stitchcache", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "strato/config/columns/hss/user_signals/api:api-strato-client", - "util/util-slf4j-api/src/main/scala", - "util/util-thrift", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicate.scala deleted file mode 100644 index 7464be7df..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicate.scala +++ /dev/null @@ -1,95 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.hss - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.finagle.util.DefaultTimer -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FilterReason -import com.twitter.follow_recommendations.common.models.FilterReason.FailOpen -import com.twitter.hss.api.thriftscala.SignalValue -import com.twitter.hss.api.thriftscala.UserHealthSignal.AgathaCseDouble -import com.twitter.hss.api.thriftscala.UserHealthSignal.NsfwAgathaUserScoreDouble -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.hss.user_signals.api.HealthSignalsOnUserClientColumn -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.logging.Logging -import com.twitter.util.Duration - -import javax.inject.Inject -import javax.inject.Singleton - -/** - * Filter out candidates based on Health Signal Store (HSS) health signals - */ -@Singleton -case class HssPredicate @Inject() ( - healthSignalsOnUserClientColumn: HealthSignalsOnUserClientColumn, - statsReceiver: StatsReceiver) - extends Predicate[(HasClientContext with HasParams, CandidateUser)] - with Logging { - - private val stats: StatsReceiver = statsReceiver.scope(this.getClass.getName) - - override def apply( - pair: (HasClientContext with HasParams, CandidateUser) - ): Stitch[PredicateResult] = { - val (request, candidate) = pair - StatsUtil.profileStitch( - getHssPredicateResult(request, candidate), - stats.scope("getHssPredicateResult") - ) - } - - private def getHssPredicateResult( - request: HasClientContext with HasParams, - candidate: CandidateUser - ): Stitch[PredicateResult] = { - - val hssCseScoreThreshold: Double = request.params(HssPredicateParams.HssCseScoreThreshold) - val hssNsfwScoreThreshold: Double = request.params(HssPredicateParams.HssNsfwScoreThreshold) - val timeout: Duration = request.params(HssPredicateParams.HssApiTimeout) - - healthSignalsOnUserClientColumn.fetcher - .fetch(candidate.id, Seq(AgathaCseDouble, NsfwAgathaUserScoreDouble)) - .map { fetchResult => - fetchResult.v match { - case Some(response) => - val agathaCseScoreDouble: Double = userHealthSignalValueToDoubleOpt( - response.signalValues.get(AgathaCseDouble)).getOrElse(0d) - val agathaNsfwScoreDouble: Double = userHealthSignalValueToDoubleOpt( - response.signalValues.get(NsfwAgathaUserScoreDouble)).getOrElse(0d) - - stats.stat("agathaCseScoreDistribution").add(agathaCseScoreDouble.toFloat) - stats.stat("agathaNsfwScoreDistribution").add(agathaNsfwScoreDouble.toFloat) - - /** - * Only filter out the candidate when it has both high Agatha CSE score and NSFW score, as the Agatha CSE - * model is an old one that may not be precise or have high recall. - */ - if (agathaCseScoreDouble >= hssCseScoreThreshold && agathaNsfwScoreDouble >= hssNsfwScoreThreshold) { - PredicateResult.Invalid(Set(FilterReason.HssSignal)) - } else { - PredicateResult.Valid - } - case None => - PredicateResult.Valid - } - } - .within(timeout)(DefaultTimer) - .rescue { - case e: Exception => - stats.scope("rescued").counter(e.getClass.getSimpleName).incr() - Stitch(PredicateResult.Invalid(Set(FailOpen))) - } - } - - private def userHealthSignalValueToDoubleOpt(signalValue: Option[SignalValue]): Option[Double] = { - signalValue match { - case Some(SignalValue.DoubleValue(value)) => Some(value) - case _ => None - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicateFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicateFSConfig.scala deleted file mode 100644 index 8cc1620a9..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicateFSConfig.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.hss - -import com.twitter.follow_recommendations.common.predicates.hss.HssPredicateParams._ -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.util.Duration - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class HssPredicateFSConfig @Inject() () extends FeatureSwitchConfig { - override val doubleFSParams: Seq[FSBoundedParam[Double]] = Seq( - HssCseScoreThreshold, - HssNsfwScoreThreshold, - ) - - override val durationFSParams: Seq[FSBoundedParam[Duration] with HasDurationConversion] = Seq( - HssApiTimeout - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicateParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicateParams.scala deleted file mode 100644 index ac6e14bbe..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health/HssPredicateParams.scala +++ /dev/null @@ -1,34 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.hss - -import com.twitter.conversions.DurationOps._ -import com.twitter.timelines.configapi.DurationConversion -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.util.Duration - -object HssPredicateParams { - object HssCseScoreThreshold - extends FSBoundedParam[Double]( - "hss_predicate_cse_score_threshold", - default = 0.992d, - min = 0.0d, - max = 1.0d) - - object HssNsfwScoreThreshold - extends FSBoundedParam[Double]( - "hss_predicate_nsfw_score_threshold", - default = 1.5d, - min = -100.0d, - max = 100.0d) - - object HssApiTimeout - extends FSBoundedParam[Duration]( - name = "hss_predicate_timeout_in_millis", - default = 200.millisecond, - min = 1.millisecond, - max = 500.millisecond) - with HasDurationConversion { - override def durationConversion: DurationConversion = DurationConversion.FromMillis - } - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/BUILD deleted file mode 100644 index 0df7b245a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/BUILD +++ /dev/null @@ -1,19 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "src/thrift/com/twitter/socialgraph:thrift-scala", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/InvalidRelationshipPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/InvalidRelationshipPredicate.scala deleted file mode 100644 index 84b8bf7a6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/InvalidRelationshipPredicate.scala +++ /dev/null @@ -1,36 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.sgs - -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FilterReason -import com.twitter.follow_recommendations.common.models.HasInvalidRelationshipUserIds -import com.twitter.stitch.Stitch -import javax.inject.Singleton - -@Singleton -class InvalidRelationshipPredicate - extends Predicate[(HasInvalidRelationshipUserIds, CandidateUser)] { - - override def apply( - pair: (HasInvalidRelationshipUserIds, CandidateUser) - ): Stitch[PredicateResult] = { - - val (targetUser, candidate) = pair - targetUser.invalidRelationshipUserIds match { - case Some(users) => - if (!users.contains(candidate.id)) { - InvalidRelationshipPredicate.ValidStitch - } else { - Stitch.value(InvalidRelationshipPredicate.InvalidRelationshipStitch) - } - case None => Stitch.value(PredicateResult.Valid) - } - } -} - -object InvalidRelationshipPredicate { - val ValidStitch: Stitch[PredicateResult.Valid.type] = Stitch.value(PredicateResult.Valid) - val InvalidRelationshipStitch: PredicateResult.Invalid = - PredicateResult.Invalid(Set(FilterReason.InvalidRelationship)) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/RecentFollowingPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/RecentFollowingPredicate.scala deleted file mode 100644 index 60f0080b8..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/RecentFollowingPredicate.scala +++ /dev/null @@ -1,33 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.sgs - -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FilterReason -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.stitch.Stitch -import javax.inject.Singleton - -@Singleton -class RecentFollowingPredicate extends Predicate[(HasRecentFollowedUserIds, CandidateUser)] { - - override def apply(pair: (HasRecentFollowedUserIds, CandidateUser)): Stitch[PredicateResult] = { - - val (targetUser, candidate) = pair - targetUser.recentFollowedUserIdsSet match { - case Some(users) => - if (!users.contains(candidate.id)) { - RecentFollowingPredicate.ValidStitch - } else { - RecentFollowingPredicate.AlreadyFollowedStitch - } - case None => RecentFollowingPredicate.ValidStitch - } - } -} - -object RecentFollowingPredicate { - val ValidStitch: Stitch[PredicateResult.Valid.type] = Stitch.value(PredicateResult.Valid) - val AlreadyFollowedStitch: Stitch[PredicateResult.Invalid] = - Stitch.value(PredicateResult.Invalid(Set(FilterReason.AlreadyFollowed))) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsPredicateFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsPredicateFSConfig.scala deleted file mode 100644 index f661dbbab..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsPredicateFSConfig.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.sgs - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.util.Duration - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class SgsPredicateFSConfig @Inject() () extends FeatureSwitchConfig { - override val durationFSParams: Seq[FSBoundedParam[Duration] with HasDurationConversion] = Seq( - SgsPredicateParams.SgsRelationshipsPredicateTimeout - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsPredicateParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsPredicateParams.scala deleted file mode 100644 index dd615c47d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsPredicateParams.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.sgs - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.DurationConversion -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.util.Duration -import com.twitter.conversions.DurationOps._ - -object SgsPredicateParams { - case object SgsRelationshipsPredicateTimeout - extends FSBoundedParam[Duration]( - name = "sgs_predicate_relationships_timeout_in_millis", - default = 300.millisecond, - min = 1.millisecond, - max = 1000.millisecond) - with HasDurationConversion { - override def durationConversion: DurationConversion = DurationConversion.FromMillis - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsRelationshipsByUserIdPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsRelationshipsByUserIdPredicate.scala deleted file mode 100644 index dec936e58..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsRelationshipsByUserIdPredicate.scala +++ /dev/null @@ -1,113 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.sgs - -import com.google.common.annotations.VisibleForTesting -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FilterReason.InvalidRelationshipTypes -import com.twitter.socialgraph.thriftscala.ExistsRequest -import com.twitter.socialgraph.thriftscala.ExistsResult -import com.twitter.socialgraph.thriftscala.LookupContext -import com.twitter.socialgraph.thriftscala.Relationship -import com.twitter.socialgraph.thriftscala.RelationshipType -import com.twitter.stitch.Stitch -import com.twitter.stitch.socialgraph.SocialGraph -import com.twitter.util.logging.Logging -import javax.inject.Inject -import javax.inject.Singleton - -class SgsRelationshipsByUserIdPredicate( - socialGraph: SocialGraph, - relationshipMappings: Seq[RelationshipMapping], - statsReceiver: StatsReceiver) - extends Predicate[(Option[Long], CandidateUser)] - with Logging { - private val InvalidFromPrimaryCandidateSourceName = "invalid_from_primary_candidate_source" - private val InvalidFromCandidateSourceName = "invalid_from_candidate_source" - private val NoPrimaryCandidateSource = "no_primary_candidate_source" - - private val stats: StatsReceiver = statsReceiver.scope(this.getClass.getName) - - override def apply( - pair: (Option[Long], CandidateUser) - ): Stitch[PredicateResult] = { - val (idOpt, candidate) = pair - val relationships = relationshipMappings.map { relationshipMapping: RelationshipMapping => - Relationship( - relationshipMapping.relationshipType, - relationshipMapping.includeBasedOnRelationship) - } - idOpt - .map { id: Long => - val existsRequest = ExistsRequest( - id, - candidate.id, - relationships = relationships, - context = SgsRelationshipsByUserIdPredicate.UnionLookupContext - ) - socialGraph - .exists(existsRequest).map { existsResult: ExistsResult => - if (existsResult.exists) { - candidate.getPrimaryCandidateSource match { - case Some(candidateSource) => - stats - .scope(InvalidFromPrimaryCandidateSourceName).counter( - candidateSource.name).incr() - case None => - stats - .scope(InvalidFromPrimaryCandidateSourceName).counter( - NoPrimaryCandidateSource).incr() - } - candidate.getCandidateSources.foreach({ - case (candidateSource, _) => - stats - .scope(InvalidFromCandidateSourceName).counter(candidateSource.name).incr() - }) - PredicateResult.Invalid(Set(InvalidRelationshipTypes(relationshipMappings - .map { relationshipMapping: RelationshipMapping => - relationshipMapping.relationshipType - }.mkString(", ")))) - } else { - PredicateResult.Valid - } - } - } - // if no user id is present, return true by default - .getOrElse(Stitch.value(PredicateResult.Valid)) - } -} - -object SgsRelationshipsByUserIdPredicate { - // OR Operation - @VisibleForTesting - private[follow_recommendations] val UnionLookupContext = Some( - LookupContext(performUnion = Some(true))) -} - -@Singleton -class ExcludeNonFollowersSgsPredicate @Inject() ( - socialGraph: SocialGraph, - statsReceiver: StatsReceiver) - extends SgsRelationshipsByUserIdPredicate( - socialGraph, - Seq(RelationshipMapping(RelationshipType.FollowedBy, includeBasedOnRelationship = false)), - statsReceiver) - -@Singleton -class ExcludeNonFollowingSgsPredicate @Inject() ( - socialGraph: SocialGraph, - statsReceiver: StatsReceiver) - extends SgsRelationshipsByUserIdPredicate( - socialGraph, - Seq(RelationshipMapping(RelationshipType.Following, includeBasedOnRelationship = false)), - statsReceiver) - -@Singleton -class ExcludeFollowingSgsPredicate @Inject() ( - socialGraph: SocialGraph, - statsReceiver: StatsReceiver) - extends SgsRelationshipsByUserIdPredicate( - socialGraph, - Seq(RelationshipMapping(RelationshipType.Following, includeBasedOnRelationship = true)), - statsReceiver) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsRelationshipsPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsRelationshipsPredicate.scala deleted file mode 100644 index fdb88ad58..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs/SgsRelationshipsPredicate.scala +++ /dev/null @@ -1,146 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.sgs - -import com.google.common.annotations.VisibleForTesting -import com.twitter.finagle.stats.NullStatsReceiver -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasProfileId -import com.twitter.follow_recommendations.common.models.FilterReason.FailOpen -import com.twitter.follow_recommendations.common.models.FilterReason.InvalidRelationshipTypes -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.socialgraph.thriftscala.ExistsRequest -import com.twitter.socialgraph.thriftscala.ExistsResult -import com.twitter.socialgraph.thriftscala.LookupContext -import com.twitter.socialgraph.thriftscala.Relationship -import com.twitter.socialgraph.thriftscala.RelationshipType -import com.twitter.stitch.Stitch -import com.twitter.stitch.socialgraph.SocialGraph -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.TimeoutException -import com.twitter.util.logging.Logging - -import javax.inject.Inject -import javax.inject.Singleton - -case class RelationshipMapping( - relationshipType: RelationshipType, - includeBasedOnRelationship: Boolean) - -class SgsRelationshipsPredicate( - socialGraph: SocialGraph, - relationshipMappings: Seq[RelationshipMapping], - statsReceiver: StatsReceiver = NullStatsReceiver) - extends Predicate[(HasClientContext with HasParams, CandidateUser)] - with Logging { - - private val stats: StatsReceiver = statsReceiver.scope(this.getClass.getSimpleName) - - override def apply( - pair: (HasClientContext with HasParams, CandidateUser) - ): Stitch[PredicateResult] = { - val (target, candidate) = pair - val timeout = target.params(SgsPredicateParams.SgsRelationshipsPredicateTimeout) - SgsRelationshipsPredicate - .extractUserId(target) - .map { id => - val relationships = relationshipMappings.map { relationshipMapping: RelationshipMapping => - Relationship( - relationshipMapping.relationshipType, - relationshipMapping.includeBasedOnRelationship) - } - val existsRequest = ExistsRequest( - id, - candidate.id, - relationships = relationships, - context = SgsRelationshipsPredicate.UnionLookupContext - ) - socialGraph - .exists(existsRequest).map { existsResult: ExistsResult => - if (existsResult.exists) { - PredicateResult.Invalid(Set(InvalidRelationshipTypes(relationshipMappings - .map { relationshipMapping: RelationshipMapping => - relationshipMapping.relationshipType - }.mkString(", ")))) - } else { - PredicateResult.Valid - } - } - .within(timeout)(com.twitter.finagle.util.DefaultTimer) - } - // if no user id is present, return true by default - .getOrElse(Stitch.value(PredicateResult.Valid)) - .rescue { - case e: TimeoutException => - stats.counter("timeout").incr() - Stitch(PredicateResult.Invalid(Set(FailOpen))) - case e: Exception => - stats.counter(e.getClass.getSimpleName).incr() - Stitch(PredicateResult.Invalid(Set(FailOpen))) - } - - } -} - -object SgsRelationshipsPredicate { - // OR Operation - @VisibleForTesting - private[follow_recommendations] val UnionLookupContext = Some( - LookupContext(performUnion = Some(true))) - - private def extractUserId(target: HasClientContext with HasParams): Option[Long] = target match { - case profRequest: HasProfileId => Some(profRequest.profileId) - case userRequest: HasClientContext with HasParams => userRequest.getOptionalUserId - case _ => None - } -} - -@Singleton -class InvalidTargetCandidateRelationshipTypesPredicate @Inject() ( - socialGraph: SocialGraph) - extends SgsRelationshipsPredicate( - socialGraph, - InvalidRelationshipTypesPredicate.InvalidRelationshipTypes) {} - -@Singleton -class NoteworthyAccountsSgsPredicate @Inject() ( - socialGraph: SocialGraph) - extends SgsRelationshipsPredicate( - socialGraph, - InvalidRelationshipTypesPredicate.NoteworthyAccountsInvalidRelationshipTypes) - -object InvalidRelationshipTypesPredicate { - - val InvalidRelationshipTypesExcludeFollowing: Seq[RelationshipMapping] = Seq( - RelationshipMapping(RelationshipType.HideRecommendations, true), - RelationshipMapping(RelationshipType.Blocking, true), - RelationshipMapping(RelationshipType.BlockedBy, true), - RelationshipMapping(RelationshipType.Muting, true), - RelationshipMapping(RelationshipType.MutedBy, true), - RelationshipMapping(RelationshipType.ReportedAsSpam, true), - RelationshipMapping(RelationshipType.ReportedAsSpamBy, true), - RelationshipMapping(RelationshipType.ReportedAsAbuse, true), - RelationshipMapping(RelationshipType.ReportedAsAbuseBy, true) - ) - - val InvalidRelationshipTypes: Seq[RelationshipMapping] = Seq( - RelationshipMapping(RelationshipType.FollowRequestOutgoing, true), - RelationshipMapping(RelationshipType.Following, true), - RelationshipMapping( - RelationshipType.UsedToFollow, - true - ) // this data is accessible for 90 days. - ) ++ InvalidRelationshipTypesExcludeFollowing - - val NoteworthyAccountsInvalidRelationshipTypes: Seq[RelationshipMapping] = Seq( - RelationshipMapping(RelationshipType.Blocking, true), - RelationshipMapping(RelationshipType.BlockedBy, true), - RelationshipMapping(RelationshipType.Muting, true), - RelationshipMapping(RelationshipType.MutedBy, true), - RelationshipMapping(RelationshipType.ReportedAsSpam, true), - RelationshipMapping(RelationshipType.ReportedAsSpamBy, true), - RelationshipMapping(RelationshipType.ReportedAsAbuse, true), - RelationshipMapping(RelationshipType.ReportedAsAbuseBy, true) - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/BUILD deleted file mode 100644 index fe3df2d8b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders", - "strato/config/columns/onboarding:onboarding-strato-client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/UserActivityPredicate.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/UserActivityPredicate.scala deleted file mode 100644 index e0fd6b42c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/UserActivityPredicate.scala +++ /dev/null @@ -1,161 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.user_activity - -import com.twitter.core_workflows.user_model.thriftscala.UserState -import com.twitter.decider.Decider -import com.twitter.decider.RandomRecipient -import com.twitter.finagle.Memcached.Client -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.PredicateResult -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.follow_recommendations.common.clients.cache.MemcacheClient -import com.twitter.follow_recommendations.common.clients.cache.ThriftEnumOptionBijection -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FilterReason -import com.twitter.follow_recommendations.configapi.deciders.DeciderKey -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.UserRecommendabilityWithLongKeysOnUserClientColumn -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject -import javax.inject.Singleton - -abstract case class UserStateActivityPredicate( - userRecommendabilityClient: UserRecommendabilityWithLongKeysOnUserClientColumn, - validCandidateStates: Set[UserState], - client: Client, - statsReceiver: StatsReceiver, - decider: Decider = Decider.False) - extends Predicate[(HasParams with HasClientContext, CandidateUser)] { - - private val stats: StatsReceiver = statsReceiver.scope(this.getClass.getSimpleName) - - // client to memcache cluster - val bijection = new ThriftEnumOptionBijection[UserState](UserState.apply) - val memcacheClient = MemcacheClient[Option[UserState]]( - client = client, - dest = "/s/cache/follow_recos_service:twemcaches", - valueBijection = bijection, - ttl = UserActivityPredicateParams.CacheTTL, - statsReceiver = stats.scope("twemcache") - ) - - override def apply( - targetAndCandidate: (HasParams with HasClientContext, CandidateUser) - ): Stitch[PredicateResult] = { - val userRecommendabilityFetcher = userRecommendabilityClient.fetcher - val (_, candidate) = targetAndCandidate - - val deciderKey: String = DeciderKey.EnableExperimentalCaching.toString - val enableDistributedCaching: Boolean = decider.isAvailable(deciderKey, Some(RandomRecipient)) - val userStateStitch: Stitch[Option[UserState]] = - enableDistributedCaching match { - case true => { - memcacheClient.readThrough( - // add a key prefix to address cache key collisions - key = "UserActivityPredicate" + candidate.id.toString, - underlyingCall = () => queryUserRecommendable(candidate.id) - ) - } - case false => queryUserRecommendable(candidate.id) - } - val resultStitch: Stitch[PredicateResult] = - userStateStitch.map { userStateOpt => - userStateOpt match { - case Some(userState) => { - if (validCandidateStates.contains(userState)) { - PredicateResult.Valid - } else { - PredicateResult.Invalid(Set(FilterReason.MinStateNotMet)) - } - } - case None => { - PredicateResult.Invalid(Set(FilterReason.MissingRecommendabilityData)) - } - } - } - - StatsUtil.profileStitch(resultStitch, stats.scope("apply")) - .rescue { - case e: Exception => - stats.scope("rescued").counter(e.getClass.getSimpleName).incr() - Stitch(PredicateResult.Invalid(Set(FilterReason.FailOpen))) - } - } - - def queryUserRecommendable( - userId: Long - ): Stitch[Option[UserState]] = { - val userRecommendabilityFetcher = userRecommendabilityClient.fetcher - userRecommendabilityFetcher.fetch(userId).map { userCandidate => - userCandidate.v.flatMap(_.userState) - } - } -} - -@Singleton -class MinStateUserActivityPredicate @Inject() ( - userRecommendabilityClient: UserRecommendabilityWithLongKeysOnUserClientColumn, - client: Client, - statsReceiver: StatsReceiver) - extends UserStateActivityPredicate( - userRecommendabilityClient, - Set( - UserState.Light, - UserState.HeavyNonTweeter, - UserState.MediumNonTweeter, - UserState.HeavyTweeter, - UserState.MediumTweeter - ), - client, - statsReceiver - ) - -@Singleton -class AllTweeterUserActivityPredicate @Inject() ( - userRecommendabilityClient: UserRecommendabilityWithLongKeysOnUserClientColumn, - client: Client, - statsReceiver: StatsReceiver) - extends UserStateActivityPredicate( - userRecommendabilityClient, - Set( - UserState.HeavyTweeter, - UserState.MediumTweeter - ), - client, - statsReceiver - ) - -@Singleton -class HeavyTweeterUserActivityPredicate @Inject() ( - userRecommendabilityClient: UserRecommendabilityWithLongKeysOnUserClientColumn, - client: Client, - statsReceiver: StatsReceiver) - extends UserStateActivityPredicate( - userRecommendabilityClient, - Set( - UserState.HeavyTweeter - ), - client, - statsReceiver - ) - -@Singleton -class NonNearZeroUserActivityPredicate @Inject() ( - userRecommendabilityClient: UserRecommendabilityWithLongKeysOnUserClientColumn, - client: Client, - statsReceiver: StatsReceiver) - extends UserStateActivityPredicate( - userRecommendabilityClient, - Set( - UserState.New, - UserState.VeryLight, - UserState.Light, - UserState.MediumNonTweeter, - UserState.MediumTweeter, - UserState.HeavyNonTweeter, - UserState.HeavyTweeter - ), - client, - statsReceiver - ) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/UserActivityPredicateParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/UserActivityPredicateParams.scala deleted file mode 100644 index 57e8d958b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity/UserActivityPredicateParams.scala +++ /dev/null @@ -1,10 +0,0 @@ -package com.twitter.follow_recommendations.common.predicates.user_activity - -import com.twitter.timelines.configapi.FSParam -import com.twitter.util.Duration - -object UserActivityPredicateParams { - case object HeavyTweeterEnabled - extends FSParam[Boolean]("user_activity_predicate_heavy_tweeter_enabled", false) - val CacheTTL: Duration = Duration.fromHours(6) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/AdhocScoreModificationType.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/AdhocScoreModificationType.scala deleted file mode 100644 index 23ccd6f4f..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/AdhocScoreModificationType.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.common - -/** - * To manage the extent of adhoc score modifications, we set a hard limit that from each of the - * types below *ONLY ONE* adhoc scorer can be applied to candidates' scores. More details about the - * usage is available in [[AdhocRanker]] - */ - -object AdhocScoreModificationType extends Enumeration { - type AdhocScoreModificationType = Value - - // This type of scorer increases the score of a subset of candidates through various policies. - val BoostingScorer: AdhocScoreModificationType = Value("boosting") - - // This type of scorer shuffles candidates randomly according to some distribution. - val WeightedRandomSamplingScorer: AdhocScoreModificationType = Value("weighted_random_sampling") - - // This is added solely for testing purposes and should not be used in production. - val InvalidAdhocScorer: AdhocScoreModificationType = Value("invalid_adhoc_scorer") -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/BUILD deleted file mode 100644 index 77d496dcf..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/BUILD +++ /dev/null @@ -1,10 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/org/slf4j:slf4j-api", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/common", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/DedupCandidates.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/DedupCandidates.scala deleted file mode 100644 index bbbde2b58..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/DedupCandidates.scala +++ /dev/null @@ -1,11 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.common - -import com.twitter.product_mixer.core.model.common.UniversalNoun -import scala.collection.mutable - -object DedupCandidates { - def apply[C <: UniversalNoun[Long]](input: Seq[C]): Seq[C] = { - val seen = mutable.HashSet[Long]() - input.filter { candidate => seen.add(candidate.id) } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/RankerId.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/RankerId.scala deleted file mode 100644 index f6fdb905a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common/RankerId.scala +++ /dev/null @@ -1,27 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.common - -object RankerId extends Enumeration { - type RankerId = Value - - val RandomRanker: RankerId = Value("random") - // The production PostNUX ML warm-start auto-retraining model ranker - val PostNuxProdRanker: RankerId = Value("postnux_prod") - val None: RankerId = Value("none") - - // Sampling from the Placket-Luce distribution. Applied after ranker step. Its ranker id is mainly used for logging. - val PlacketLuceSamplingTransformer: RankerId = Value("placket_luce_sampling_transformer") - - def getRankerByName(name: String): Option[RankerId] = - RankerId.values.toSeq.find(_.equals(Value(name))) - -} - -/** - * ML model based heavy ranker ids. - */ -object ModelBasedHeavyRankerId { - import RankerId._ - val HeavyRankerIds: Set[String] = Set( - PostNuxProdRanker.toString, - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/BUILD deleted file mode 100644 index 2fce8d77a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRanker.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRanker.scala deleted file mode 100644 index 18ac0436b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRanker.scala +++ /dev/null @@ -1,141 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.fatigue_ranker - -import com.twitter.finagle.stats.Counter -import com.twitter.finagle.stats.Stat -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.Ranker -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasWtfImpressions -import com.twitter.follow_recommendations.common.models.WtfImpression -import com.twitter.follow_recommendations.common.rankers.common.RankerId.RankerId -import com.twitter.follow_recommendations.common.rankers.utils.Utils -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.servo.util.MemoizingStatsReceiver -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.Time - -/** - * Ranks candidates based on the given weights for each algorithm while preserving the ranks inside each algorithm. - * Reorders the ranked list based on recent impressions from recentImpressionRepo - * - * Note that the penalty is added to the rank of each candidate. To make producer-side experiments - * with multiple rankers possible, we modify the scores for each candidate and ranker as: - * NewScore(C, R) = -(Rank(C, R) + Impression(C, U) x FatigueFactor), - * where C is a candidate, R a ranker and U the target user. - * Note also that fatigue penalty is independent of any of the rankers. - */ -class ImpressionBasedFatigueRanker[ - Target <: HasClientContext with HasDisplayLocation with HasParams with HasWtfImpressions -]( - fatigueFactor: Int, - statsReceiver: StatsReceiver) - extends Ranker[Target, CandidateUser] { - - val name: String = this.getClass.getSimpleName - val stats = statsReceiver.scope("impression_based_fatigue_ranker") - val droppedStats: MemoizingStatsReceiver = new MemoizingStatsReceiver(stats.scope("hard_drops")) - val impressionStats: StatsReceiver = stats.scope("wtf_impressions") - val noImpressionCounter: Counter = impressionStats.counter("no_impressions") - val oldestImpressionStat: Stat = impressionStats.stat("oldest_sec") - - override def rank(target: Target, candidates: Seq[CandidateUser]): Stitch[Seq[CandidateUser]] = { - StatsUtil.profileStitch( - Stitch.value(rankCandidates(target, candidates)), - stats.scope("rank") - ) - } - - private def trackTimeSinceOldestImpression(impressions: Seq[WtfImpression]): Unit = { - val timeSinceOldest = Time.now - impressions.map(_.latestTime).min - oldestImpressionStat.add(timeSinceOldest.inSeconds) - } - - private def rankCandidates( - target: Target, - candidates: Seq[CandidateUser] - ): Seq[CandidateUser] = { - target.wtfImpressions - .map { wtfImpressions => - if (wtfImpressions.isEmpty) { - noImpressionCounter.incr() - candidates - } else { - val rankerIds = - candidates.flatMap(_.scores.map(_.scores.flatMap(_.rankerId))).flatten.sorted.distinct - - /** - * In below we create a Map from each CandidateUser's ID to a Map from each Ranker that - * the user has a score for, and candidate's corresponding rank when candidates are sorted - * by that Ranker (Only candidates who have this Ranker are considered for ranking). - */ - val candidateRanks: Map[Long, Map[RankerId, Int]] = rankerIds - .flatMap { rankerId => - // Candidates with no scores from this Ranker is first removed to calculate ranks. - val relatedCandidates = - candidates.filter(_.scores.exists(_.scores.exists(_.rankerId.contains(rankerId)))) - relatedCandidates - .sortBy(-_.scores - .flatMap(_.scores.find(_.rankerId.contains(rankerId)).map(_.value)).getOrElse( - 0.0)).zipWithIndex.map { - case (candidate, rank) => (candidate.id, rankerId, rank) - } - }.groupBy(_._1).map { - case (candidate, ranksForAllRankers) => - ( - candidate, - ranksForAllRankers.map { case (_, rankerId, rank) => (rankerId, rank) }.toMap) - } - - val idFatigueCountMap = - wtfImpressions.groupBy(_.candidateId).mapValues(_.map(_.counts).sum) - trackTimeSinceOldestImpression(wtfImpressions) - val rankedCandidates: Seq[CandidateUser] = candidates - .map { candidate => - val candidateImpressions = idFatigueCountMap.getOrElse(candidate.id, 0) - val fatiguedScores = candidate.scores.map { ss => - ss.copy(scores = ss.scores.map { s => - s.rankerId match { - // We set the new score as -rank after fatigue penalty is applied. - case Some(rankerId) => - // If the candidate's ID is not in the candidate->ranks map, or there is no - // rank for this specific ranker and this candidate, we use maximum possible - // rank instead. Note that this indicates that there is a problem. - s.copy(value = -(candidateRanks - .getOrElse(candidate.id, Map()).getOrElse(rankerId, candidates.length) + - candidateImpressions * fatigueFactor)) - // In case a score exists without a RankerId, we pass on the score as is. - case None => s - } - }) - } - candidate.copy(scores = fatiguedScores) - }.zipWithIndex.map { - // We re-rank candidates with their input ordering (which is done by the request-level - // ranker) and fatigue penalty. - case (candidate, inputRank) => - val candidateImpressions = idFatigueCountMap.getOrElse(candidate.id, 0) - (candidate, inputRank + candidateImpressions * fatigueFactor) - }.sortBy(_._2).map(_._1) - // Only populate ranking info when WTF impression info present - val scribeRankingInfo: Boolean = - target.params(ImpressionBasedFatigueRankerParams.ScribeRankingInfoInFatigueRanker) - if (scribeRankingInfo) Utils.addRankingInfo(rankedCandidates, name) else rankedCandidates - } - }.getOrElse(candidates) // no reranking/filtering when wtf impressions not present - } -} - -object ImpressionBasedFatigueRanker { - val DefaultFatigueFactor = 5 - - def build[ - Target <: HasClientContext with HasDisplayLocation with HasParams with HasWtfImpressions - ]( - baseStatsReceiver: StatsReceiver, - fatigueFactor: Int = DefaultFatigueFactor - ): ImpressionBasedFatigueRanker[Target] = - new ImpressionBasedFatigueRanker(fatigueFactor, baseStatsReceiver) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRankerFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRankerFSConfig.scala deleted file mode 100644 index 34fbbeb46..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRankerFSConfig.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.fatigue_ranker - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSParam -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ImpressionBasedFatigueRankerFSConfig @Inject() extends FeatureSwitchConfig { - override val booleanFSParams: Seq[FSParam[Boolean]] = - Seq(ImpressionBasedFatigueRankerParams.ScribeRankingInfoInFatigueRanker) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRankerParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRankerParams.scala deleted file mode 100644 index 075d78bf6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker/ImpressionBasedFatigueRankerParams.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.fatigue_ranker - -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.Param - -object ImpressionBasedFatigueRankerParams { - // Whether to enable hard dropping of impressed candidates - object DropImpressedCandidateEnabled extends Param[Boolean](false) - // At what # of impressions to hard drop candidates. - object DropCandidateImpressionThreshold extends Param[Int](default = 10) - // Whether to scribe candidate ranking/scoring info per ranking stage - object ScribeRankingInfoInFatigueRanker - extends FSParam[Boolean]("fatigue_ranker_scribe_ranking_info", true) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/BUILD deleted file mode 100644 index 3de5523b1..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRanker.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRanker.scala deleted file mode 100644 index 8fbeafa25..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRanker.scala +++ /dev/null @@ -1,115 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.first_n_ranker - -import com.google.inject.Inject -import com.google.inject.Singleton -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.Ranker -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasQualityFactor -import com.twitter.follow_recommendations.common.rankers.utils.Utils -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -/** - * This class is meant to filter candidates between stages of our ranker by taking the first N - * candidates, merging any candidate source information for candidates with multiple entries. - * To allow us to chain this truncation operation any number of times sequentially within the main - * ranking builder, we abstract the truncation as a separate Ranker - */ -@Singleton -class FirstNRanker[Target <: HasClientContext with HasParams with HasQualityFactor] @Inject() ( - stats: StatsReceiver) - extends Ranker[Target, CandidateUser] { - - val name: String = this.getClass.getSimpleName - private val baseStats = stats.scope("first_n_ranker") - val scaledDownByQualityFactorCounter = - baseStats.counter("scaled_down_by_quality_factor") - private val mergeStat = baseStats.scope("merged_candidates") - private val mergeStat2 = mergeStat.counter("2") - private val mergeStat3 = mergeStat.counter("3") - private val mergeStat4 = mergeStat.counter("4+") - private val candidateSizeStats = baseStats.scope("candidate_size") - - private case class CandidateSourceScore( - candidateId: Long, - sourceId: CandidateSourceIdentifier, - score: Option[Double]) - - /** - * Adds the rank of each candidate based on the primary candidate source's score. - * In the event where the provided ordering of candidates do not align with the score, - * we will respect the score, since the ordering might have been mixed up due to other previous - * steps like the shuffleFn in the `WeightedCandidateSourceRanker`. - * @param candidates ordered list of candidates - * @return same ordered list of candidates, but with the rank information appended - */ - def addRank(candidates: Seq[CandidateUser]): Seq[CandidateUser] = { - val candidateSourceRanks = for { - (sourceIdOpt, sourceCandidates) <- candidates.groupBy(_.getPrimaryCandidateSource) - (candidate, rank) <- sourceCandidates.sortBy(-_.score.getOrElse(0.0)).zipWithIndex - } yield { - (candidate, sourceIdOpt) -> rank - } - candidates.map { c => - c.getPrimaryCandidateSource - .map { sourceId => - val sourceRank = candidateSourceRanks((c, c.getPrimaryCandidateSource)) - c.addCandidateSourceRanksMap(Map(sourceId -> sourceRank)) - }.getOrElse(c) - } - } - - override def rank(target: Target, candidates: Seq[CandidateUser]): Stitch[Seq[CandidateUser]] = { - - val scaleDownFactor = Math.max( - target.qualityFactor.getOrElse(1.0d), - target.params(FirstNRankerParams.MinNumCandidatesScoredScaleDownFactor) - ) - - if (scaleDownFactor < 1.0d) - scaledDownByQualityFactorCounter.incr() - - val n = (target.params(FirstNRankerParams.CandidatesToRank) * scaleDownFactor).toInt - val scribeRankingInfo: Boolean = - target.params(FirstNRankerParams.ScribeRankingInfoInFirstNRanker) - candidateSizeStats.counter(s"n$n").incr() - val candidatesWithRank = addRank(candidates) - if (target.params(FirstNRankerParams.GroupDuplicateCandidates)) { - val groupedCandidates: Map[Long, Seq[CandidateUser]] = candidatesWithRank.groupBy(_.id) - val topN = candidates - .map { c => - merge(groupedCandidates(c.id)) - }.distinct.take(n) - Stitch.value(if (scribeRankingInfo) Utils.addRankingInfo(topN, name) else topN) - } else { - Stitch.value( - if (scribeRankingInfo) Utils.addRankingInfo(candidatesWithRank, name).take(n) - else candidatesWithRank.take(n)) - } // for efficiency, if don't need to deduplicate - } - - /** - * we use the primary candidate source of the first entry, and aggregate all of the other entries' - * candidate source scores into the first entry's candidateSourceScores - * @param candidates list of candidates with the same id - * @return a single merged candidate - */ - private[first_n_ranker] def merge(candidates: Seq[CandidateUser]): CandidateUser = { - if (candidates.size == 1) { - candidates.head - } else { - candidates.size match { - case 2 => mergeStat2.incr() - case 3 => mergeStat3.incr() - case i if i >= 4 => mergeStat4.incr() - case _ => - } - val allSources = candidates.flatMap(_.getCandidateSources).toMap - val allRanks = candidates.flatMap(_.getCandidateRanks).toMap - candidates.head.addCandidateSourceScoresMap(allSources).addCandidateSourceRanksMap(allRanks) - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerFSConfig.scala deleted file mode 100644 index 484738dc1..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerFSConfig.scala +++ /dev/null @@ -1,21 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.first_n_ranker - -import javax.inject.Inject -import javax.inject.Singleton -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam - -@Singleton -class FirstNRankerFSConfig @Inject() extends FeatureSwitchConfig { - override val booleanFSParams: Seq[FSParam[Boolean]] = - Seq(FirstNRankerParams.ScribeRankingInfoInFirstNRanker) - - override val intFSParams: Seq[FSBoundedParam[Int]] = Seq( - FirstNRankerParams.CandidatesToRank - ) - - override val doubleFSParams: Seq[FSBoundedParam[Double]] = Seq( - FirstNRankerParams.MinNumCandidatesScoredScaleDownFactor - ) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerFeatureSwitchKeys.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerFeatureSwitchKeys.scala deleted file mode 100644 index 682b60fed..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerFeatureSwitchKeys.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.first_n_ranker - -object FirstNRankerFeatureSwitchKeys { - val CandidatePoolSize = "first_n_ranker_candidate_pool_size" - val ScribeRankingInfo = "first_n_ranker_scribe_ranking_info" - val MinNumCandidatesScoredScaleDownFactor = - "first_n_ranker_min_scale_down_factor" -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerParams.scala deleted file mode 100644 index ac65a6dde..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker/FirstNRankerParams.scala +++ /dev/null @@ -1,26 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.first_n_ranker - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.Param - -object FirstNRankerParams { - case object CandidatesToRank - extends FSBoundedParam[Int]( - FirstNRankerFeatureSwitchKeys.CandidatePoolSize, - default = 100, - min = 50, - max = 600) - - case object GroupDuplicateCandidates extends Param[Boolean](true) - case object ScribeRankingInfoInFirstNRanker - extends FSParam[Boolean](FirstNRankerFeatureSwitchKeys.ScribeRankingInfo, true) - - // the minimum of candidates to score in each request. - object MinNumCandidatesScoredScaleDownFactor - extends FSBoundedParam[Double]( - name = FirstNRankerFeatureSwitchKeys.MinNumCandidatesScoredScaleDownFactor, - default = 0.3, - min = 0.1, - max = 1.0) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/BUILD deleted file mode 100644 index e71d8a7ab..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/BUILD +++ /dev/null @@ -1,21 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRanker.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRanker.scala deleted file mode 100644 index 973275f51..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRanker.scala +++ /dev/null @@ -1,204 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.interleave_ranker - -import com.google.common.annotations.VisibleForTesting -import com.google.inject.Inject -import com.google.inject.Singleton -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.Ranker -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.rankers.common.RankerId -import com.twitter.follow_recommendations.common.rankers.utils.Utils -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -@Singleton -class InterleaveRanker[Target <: HasParams] @Inject() ( - statsReceiver: StatsReceiver) - extends Ranker[Target, CandidateUser] { - - val name: String = this.getClass.getSimpleName - private val stats = statsReceiver.scope("interleave_ranker") - private val inputStats = stats.scope("input") - private val interleavingStats = stats.scope("interleave") - - override def rank( - target: Target, - candidates: Seq[CandidateUser] - ): Stitch[Seq[CandidateUser]] = { - StatsUtil.profileStitch( - Stitch.value(rankCandidates(target, candidates)), - stats.scope("rank") - ) - } - - private def rankCandidates( - target: Target, - candidates: Seq[CandidateUser] - ): Seq[CandidateUser] = { - - /** - * By this stage, all valid candidates should have: - * 1. Their Scores field populated. - * 2. Their selectedRankerId set. - * 3. Have a score associated to their selectedRankerId. - * If there is any candidate that doesn't meet the conditions above, there is a problem in one - * of the previous rankers. Since no new scoring is done in this ranker, we simply remove them. - */ - val validCandidates = - candidates.filter { c => - c.scores.isDefined && - c.scores.exists(_.selectedRankerId.isDefined) && - getCandidateScoreByRankerId(c, c.scores.flatMap(_.selectedRankerId)).isDefined - } - - // To monitor the percentage of valid candidates, as defined above, we track the following: - inputStats.counter("candidates_with_no_scores").incr(candidates.count(_.scores.isEmpty)) - inputStats - .counter("candidates_with_no_selected_ranker").incr(candidates.count { c => - c.scores.isEmpty || c.scores.exists(_.selectedRankerId.isEmpty) - }) - inputStats - .counter("candidates_with_no_score_for_selected_ranker").incr(candidates.count { c => - c.scores.isEmpty || - c.scores.exists(_.selectedRankerId.isEmpty) || - getCandidateScoreByRankerId(c, c.scores.flatMap(_.selectedRankerId)).isEmpty - }) - inputStats.counter("total_num_candidates").incr(candidates.length) - inputStats.counter("total_valid_candidates").incr(validCandidates.length) - - // We only count rankerIds from those candidates who are valid to exclude those candidates with - // a valid selectedRankerId that don't have an associated score for it. - val rankerIds = validCandidates.flatMap(_.scores.flatMap(_.selectedRankerId)).sorted.distinct - rankerIds.foreach { rankerId => - inputStats - .counter(s"valid_scores_for_${rankerId.toString}").incr( - candidates.count(getCandidateScoreByRankerId(_, Some(rankerId)).isDefined)) - inputStats.counter(s"total_candidates_for_${rankerId.toString}").incr(candidates.length) - } - inputStats.counter(s"num_ranker_ids=${rankerIds.length}").incr() - val scribeRankingInfo: Boolean = - target.params(InterleaveRankerParams.ScribeRankingInfoInInterleaveRanker) - if (rankerIds.length <= 1) - // In the case of "Number of RankerIds = 0", we pass on the candidates even though there is - // a problem in a previous ranker that provided the scores. - if (scribeRankingInfo) Utils.addRankingInfo(candidates, name) else candidates - else - if (scribeRankingInfo) - Utils.addRankingInfo(interleaveCandidates(validCandidates, rankerIds), name) - else interleaveCandidates(validCandidates, rankerIds) - } - - @VisibleForTesting - private[interleave_ranker] def interleaveCandidates( - candidates: Seq[CandidateUser], - rankerIds: Seq[RankerId.RankerId] - ): Seq[CandidateUser] = { - val candidatesWithRank = rankerIds - .flatMap { ranker => - candidates - // We first sort all candidates using this ranker. - .sortBy(-getCandidateScoreByRankerId(_, Some(ranker)).getOrElse(Double.MinValue)) - .zipWithIndex.filter( - // but only hold those candidates whose selected ranker is this ranker. - // These ranks will be forced in the final ordering. - _._1.scores.flatMap(_.selectedRankerId).contains(ranker)) - } - - // Only candidates who have isInProducerScoringExperiment set to true will have their position enforced. We - // separate candidates into two groups: (1) Production and (2) Experiment. - val (expCandidates, prodCandidates) = - candidatesWithRank.partition(_._1.scores.exists(_.isInProducerScoringExperiment)) - - // We resolve (potential) conflicts between the enforced ranks of experimental models. - val expCandidatesFinalPos = resolveConflicts(expCandidates) - - // Retrieve non-occupied positions and assign them to candidates who use production ranker. - val occupiedPos = expCandidatesFinalPos.map(_._2).toSet - val prodCandidatesFinalPos = - prodCandidates - .map(_._1).zip( - candidates.indices.filterNot(occupiedPos.contains).sorted.take(prodCandidates.length)) - - // Merge the two groups and sort them by their corresponding positions. - val finalCandidates = (prodCandidatesFinalPos ++ expCandidatesFinalPos).sortBy(_._2).map(_._1) - - // We count the presence of each ranker in the top-3 final positions. - finalCandidates.zip(0 until 3).foreach { - case (c, r) => - // We only do so for candidates that are in a producer-side experiment. - if (c.scores.exists(_.isInProducerScoringExperiment)) - c.scores.flatMap(_.selectedRankerId).map(_.toString).foreach { rankerName => - interleavingStats - .counter(s"num_final_position_${r}_$rankerName") - .incr() - } - } - - finalCandidates - } - - @VisibleForTesting - private[interleave_ranker] def resolveConflicts( - candidatesWithRank: Seq[(CandidateUser, Int)] - ): Seq[(CandidateUser, Int)] = { - // The following two metrics will allow us to calculate the rate of conflicts occurring. - // Example: If overall there are 10 producers in different bucketing experiments, and 3 of them - // are assigned to the same position. The rate would be 3/10, 30%. - val numCandidatesWithConflicts = interleavingStats.counter("candidates_with_conflict") - val numCandidatesNoConflicts = interleavingStats.counter("candidates_without_conflict") - val candidatesGroupedByRank = candidatesWithRank.groupBy(_._2).toSeq.sortBy(_._1).map { - case (rank, candidatesWithRank) => (rank, candidatesWithRank.map(_._1)) - } - - candidatesGroupedByRank.foldLeft(Seq[(CandidateUser, Int)]()) { (upToHere, nextGroup) => - val (rank, candidates) = nextGroup - if (candidates.length > 1) - numCandidatesWithConflicts.incr(candidates.length) - else - numCandidatesNoConflicts.incr() - - // We use the position after the last-assigned candidate as a starting point, or 0 otherwise. - // If candidates' position is after this "starting point", we enforce that position instead. - val minAvailableIndex = scala.math.max(upToHere.lastOption.map(_._2).getOrElse(-1) + 1, rank) - val enforcedPos = - (minAvailableIndex until minAvailableIndex + candidates.length).toList - val shuffledEnforcedPos = - if (candidates.length > 1) scala.util.Random.shuffle(enforcedPos) else enforcedPos - if (shuffledEnforcedPos.length > 1) { - candidates.zip(shuffledEnforcedPos).sortBy(_._2).map(_._1).zipWithIndex.foreach { - case (c, r) => - c.scores.flatMap(_.selectedRankerId).map(_.toString).foreach { rankerName => - // For each ranker, we count the total number of times it has been in a conflict. - interleavingStats - .counter(s"num_${shuffledEnforcedPos.length}-way_conflicts_$rankerName") - .incr() - // We also count the positions each of the rankers have fallen randomly into. In any - // experiment this should converge to uniform distribution given enough occurrences. - // Note that the position here is relative to the other candidates in the conflict and - // not the overall position of each candidate. - interleavingStats - .counter( - s"num_position_${r}_after_${shuffledEnforcedPos.length}-way_conflict_$rankerName") - .incr() - } - } - } - upToHere ++ candidates.zip(shuffledEnforcedPos).sortBy(_._2) - } - } - - @VisibleForTesting - private[interleave_ranker] def getCandidateScoreByRankerId( - candidate: CandidateUser, - rankerIdOpt: Option[RankerId.RankerId] - ): Option[Double] = { - rankerIdOpt match { - case None => None - case Some(rankerId) => - candidate.scores.flatMap { - _.scores.find(_.rankerId.contains(rankerId)).map(_.value) - } - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRankerFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRankerFSConfig.scala deleted file mode 100644 index 5a8c6de2a..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRankerFSConfig.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.interleave_ranker - -import javax.inject.Inject -import javax.inject.Singleton -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSParam - -@Singleton -class InterleaveRankerFSConfig @Inject() extends FeatureSwitchConfig { - override val booleanFSParams: Seq[FSParam[Boolean]] = - Seq(InterleaveRankerParams.ScribeRankingInfoInInterleaveRanker) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRankerParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRankerParams.scala deleted file mode 100644 index 84e6ea314..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker/InterleaveRankerParams.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.interleave_ranker - -import com.twitter.timelines.configapi.FSParam - -object InterleaveRankerParams { - case object ScribeRankingInfoInInterleaveRanker - extends FSParam[Boolean]("interleave_ranker_scribe_ranking_info", true) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/BUILD deleted file mode 100644 index 0c277e005..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/BUILD +++ /dev/null @@ -1,37 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "src/java/com/twitter/ml/api:api-base", - "util/util-slf4j-api/src/main/scala", - ], -) - -# This is to import only the params from MlRanker, for instance to get request-level heavy ranker. -scala_library( - name = "ml_ranker_params", - sources = [ - "MlRankerParams.scala", - ], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common", - "timelines/src/main/scala/com/twitter/timelines/config/configapi", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/HydrateFeaturesTransform.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/HydrateFeaturesTransform.scala deleted file mode 100644 index 499a14b67..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/HydrateFeaturesTransform.scala +++ /dev/null @@ -1,57 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.ml_ranker.ranking - -import com.google.inject.Inject -import com.google.inject.Singleton -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.GatedTransform -import com.twitter.follow_recommendations.common.base.StatsUtil.profileStitchMapResults -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.feature_hydration.sources.UserScoringFeatureSource -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDebugOptions -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasSimilarToContext -import com.twitter.follow_recommendations.common.models.RichDataRecord -import com.twitter.ml.api.DataRecord -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.logging.Logging - -/** - * Hydrate features given target and candidates lists. - * This is a required step before MlRanker. - * If a feature is not hydrated before MlRanker is triggered, a runtime exception will be thrown - */ -@Singleton -class HydrateFeaturesTransform[ - Target <: HasClientContext with HasParams with HasDebugOptions with HasPreFetchedFeature with HasSimilarToContext with HasDisplayLocation] @Inject() ( - userScoringFeatureSource: UserScoringFeatureSource, - stats: StatsReceiver) - extends GatedTransform[Target, CandidateUser] - with Logging { - - private val hydrateFeaturesStats = stats.scope("hydrate_features") - - def transform(target: Target, candidates: Seq[CandidateUser]): Stitch[Seq[CandidateUser]] = { - // get features - val featureMapStitch: Stitch[Map[CandidateUser, DataRecord]] = - profileStitchMapResults( - userScoringFeatureSource.hydrateFeatures(target, candidates), - hydrateFeaturesStats) - - featureMapStitch.map { featureMap => - candidates - .map { candidate => - val dataRecord = featureMap(candidate) - // add debugRecord only when the request parameter is set - val debugDataRecord = if (target.debugOptions.exists(_.fetchDebugInfo)) { - Some(candidate.toDebugDataRecord(dataRecord, userScoringFeatureSource.featureContext)) - } else None - candidate.copy( - dataRecord = Some(RichDataRecord(Some(dataRecord), debugDataRecord)) - ) - } - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRanker.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRanker.scala deleted file mode 100644 index 6344c348e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRanker.scala +++ /dev/null @@ -1,219 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.ml_ranker.ranking - -import com.google.common.annotations.VisibleForTesting -import com.google.inject.Inject -import com.google.inject.Singleton -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.Ranker -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.follow_recommendations.common.base.StatsUtil.profileSeqResults -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasDebugOptions -import com.twitter.follow_recommendations.common.models.Scores -import com.twitter.follow_recommendations.common.rankers.common.RankerId -import com.twitter.follow_recommendations.common.rankers.common.RankerId.RankerId -import com.twitter.follow_recommendations.common.rankers.utils.Utils -import com.twitter.follow_recommendations.common.rankers.ml_ranker.scoring.AdhocScorer -import com.twitter.follow_recommendations.common.rankers.ml_ranker.scoring.Scorer -import com.twitter.follow_recommendations.common.rankers.ml_ranker.scoring.ScorerFactory -import com.twitter.follow_recommendations.common.utils.CollectionUtil -import com.twitter.ml.api.DataRecord -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Params -import com.twitter.util.logging.Logging - -/** - * This class has a rank function that will perform 4 steps: - * - choose which scorer to use for each candidate - * - score candidates given their respective features - * - add scoring information to the candidate - * - sort candidates by their respective scores - * The feature source and scorer will depend on the request's params - */ -@Singleton -class MlRanker[ - Target <: HasClientContext with HasParams with HasDisplayLocation with HasDebugOptions] @Inject() ( - scorerFactory: ScorerFactory, - statsReceiver: StatsReceiver) - extends Ranker[Target, CandidateUser] - with Logging { - - private val stats: StatsReceiver = statsReceiver.scope("ml_ranker") - - private val inputStat = stats.scope("1_input") - private val selectScorerStat = stats.scope("2_select_scorer") - private val scoreStat = stats.scope("3_score") - - override def rank( - target: Target, - candidates: Seq[CandidateUser] - ): Stitch[Seq[CandidateUser]] = { - profileSeqResults(candidates, inputStat) - val requestRankerId = target.params(MlRankerParams.RequestScorerIdParam) - val rankerIds = chooseRankerByCandidate(candidates, requestRankerId) - - val scoreStitch = score(candidates, rankerIds, requestRankerId).map { scoredCandidates => - { - // sort the candidates by score - val sortedCandidates = sort(target, scoredCandidates) - // add scribe field to candidates (if applicable) and return candidates - scribeCandidates(target, sortedCandidates) - } - } - StatsUtil.profileStitch(scoreStitch, stats.scope("rank")) - } - - /** - * @param target: The WTF request for a given consumer. - * @param candidates A list of candidates considered for recommendation. - * @return A map from each candidate to a tuple that includes: - * (1) The selected scorer that should be used to rank this candidate - * (2) a flag determining whether the candidate is in a producer-side experiment. - */ - private[ranking] def chooseRankerByCandidate( - candidates: Seq[CandidateUser], - requestRankerId: RankerId - ): Map[CandidateUser, RankerId] = { - candidates.map { candidate => - val selectedCandidateRankerId = - if (candidate.params == Params.Invalid || candidate.params == Params.Empty) { - selectScorerStat.counter("candidate_params_empty").incr() - requestRankerId - } else { - val candidateRankerId = candidate.params(MlRankerParams.CandidateScorerIdParam) - if (candidateRankerId == RankerId.None) { - // This candidate is a not part of any producer-side experiment. - selectScorerStat.counter("default_to_request_ranker").incr() - requestRankerId - } else { - // This candidate is in a treatment bucket of a producer-side experiment. - selectScorerStat.counter("use_candidate_ranker").incr() - candidateRankerId - } - } - selectScorerStat.scope("selected").counter(selectedCandidateRankerId.toString).incr() - candidate -> selectedCandidateRankerId - }.toMap - } - - @VisibleForTesting - private[ranking] def score( - candidates: Seq[CandidateUser], - rankerIds: Map[CandidateUser, RankerId], - requestRankerId: RankerId - ): Stitch[Seq[CandidateUser]] = { - val features = candidates.map(_.dataRecord.flatMap(_.dataRecord)) - - require(features.forall(_.nonEmpty), "features are not hydrated for all the candidates") - - val scorers = scorerFactory.getScorers(rankerIds.values.toSeq.sorted.distinct) - - // Scorers are split into ML-based and Adhoc (defined as a scorer that does not need to call an - // ML prediction service and scores candidates using locally-available data). - val (adhocScorers, mlScorers) = scorers.partition { - case _: AdhocScorer => true - case _ => false - } - - // score candidates - val scoresStitch = score(features.map(_.get), mlScorers) - val candidatesWithMlScoresStitch = scoresStitch.map { scoresSeq => - candidates - .zip(scoresSeq).map { // copy datarecord and score into candidate object - case (candidate, scores) => - val selectedRankerId = rankerIds(candidate) - val useRequestRanker = - candidate.params == Params.Invalid || - candidate.params == Params.Empty || - candidate.params(MlRankerParams.CandidateScorerIdParam) == RankerId.None - candidate.copy( - score = scores.scores.find(_.rankerId.contains(requestRankerId)).map(_.value), - scores = if (scores.scores.nonEmpty) { - Some( - scores.copy( - scores = scores.scores, - selectedRankerId = Some(selectedRankerId), - isInProducerScoringExperiment = !useRequestRanker - )) - } else None - ) - } - } - - candidatesWithMlScoresStitch.map { candidates => - // The basis for adhoc scores are the "request-level" ML ranker. We add the base score here - // while adhoc scorers are applied in [[AdhocRanker]]. - addMlBaseScoresForAdhocScorers(candidates, requestRankerId, adhocScorers) - } - } - - @VisibleForTesting - private[ranking] def addMlBaseScoresForAdhocScorers( - candidates: Seq[CandidateUser], - requestRankerId: RankerId, - adhocScorers: Seq[Scorer] - ): Seq[CandidateUser] = { - candidates.map { candidate => - candidate.scores match { - case Some(oldScores) => - // 1. We fetch the ML score that is the basis of adhoc scores: - val baseMlScoreOpt = Utils.getCandidateScoreByRankerId(candidate, requestRankerId) - - // 2. For each adhoc scorer, we copy the ML score object, changing only the ID and type. - val newScores = adhocScorers flatMap { adhocScorer => - baseMlScoreOpt.map( - _.copy(rankerId = Some(adhocScorer.id), scoreType = adhocScorer.scoreType)) - } - - // 3. We add the new adhoc score entries to the candidate. - candidate.copy(scores = Some(oldScores.copy(scores = oldScores.scores ++ newScores))) - case _ => - // Since there is no base ML score, there should be no adhoc score modification as well. - candidate - } - } - } - - private[this] def score( - dataRecords: Seq[DataRecord], - scorers: Seq[Scorer] - ): Stitch[Seq[Scores]] = { - val scoredResponse = scorers.map { scorer => - StatsUtil.profileStitch(scorer.score(dataRecords), scoreStat.scope(scorer.id.toString)) - } - // If we could score a candidate with too many rankers, it is likely to blow up the whole system. - // and fail back to default production model - StatsUtil.profileStitch(Stitch.collect(scoredResponse), scoreStat).map { scoresByScorerId => - CollectionUtil.transposeLazy(scoresByScorerId).map { scoresPerCandidate => - Scores(scoresPerCandidate) - } - } - } - - // sort candidates using score in descending order - private[this] def sort( - target: Target, - candidates: Seq[CandidateUser] - ): Seq[CandidateUser] = { - candidates.sortBy(c => -c.score.getOrElse(MlRanker.DefaultScore)) - } - - private[this] def scribeCandidates( - target: Target, - candidates: Seq[CandidateUser] - ): Seq[CandidateUser] = { - val scribeRankingInfo: Boolean = target.params(MlRankerParams.ScribeRankingInfoInMlRanker) - scribeRankingInfo match { - case true => Utils.addRankingInfo(candidates, "MlRanker") - case false => candidates - } - } -} - -object MlRanker { - // this is to ensure candidates with absent scores are ranked the last - val DefaultScore: Double = Double.MinValue -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRankerFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRankerFSConfig.scala deleted file mode 100644 index c69a32fc5..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRankerFSConfig.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.ml_ranker.ranking - -import javax.inject.Inject -import javax.inject.Singleton -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSParam - -@Singleton -class MlRankerFSConfig @Inject() extends FeatureSwitchConfig { - override val booleanFSParams: Seq[FSParam[Boolean]] = - Seq(MlRankerParams.ScribeRankingInfoInMlRanker) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRankerParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRankerParams.scala deleted file mode 100644 index 8463963a6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking/MlRankerParams.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.ml_ranker.ranking - -import com.twitter.follow_recommendations.common.rankers.common.RankerId -import com.twitter.timelines.configapi.FSEnumParam -import com.twitter.timelines.configapi.FSParam - -/** - * When adding Producer side experiments, make sure to register the FS Key in [[ProducerFeatureFilter]] - * in [[FeatureSwitchesModule]], otherwise, the FS will not work. - */ -object MlRankerParams { - // which ranker to use by default for the given request - case object RequestScorerIdParam - extends FSEnumParam[RankerId.type]( - name = "post_nux_ml_flow_ml_ranker_id", - default = RankerId.PostNuxProdRanker, - enum = RankerId - ) - - // which ranker to use for the given candidate - case object CandidateScorerIdParam - extends FSEnumParam[RankerId.type]( - name = "post_nux_ml_flow_candidate_user_scorer_id", - default = RankerId.None, - enum = RankerId - ) - - case object ScribeRankingInfoInMlRanker - extends FSParam[Boolean]("post_nux_ml_flow_scribe_ranking_info_in_ml_ranker", true) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/AdhocScorer.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/AdhocScorer.scala deleted file mode 100644 index 39921bb71..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/AdhocScorer.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.ml_ranker.scoring - -import com.twitter.follow_recommendations.common.rankers.common.AdhocScoreModificationType.AdhocScoreModificationType -import com.twitter.follow_recommendations.common.models.Score -import com.twitter.ml.api.DataRecord -import com.twitter.stitch.Stitch - -trait AdhocScorer extends Scorer { - - /** - * NOTE: For instances of [[AdhocScorer]] this function SHOULD NOT be used. - * Please use: - * [[score(target: HasClientContext with HasParams, candidates: Seq[CandidateUser])]] - * instead. - */ - @Deprecated - override def score(records: Seq[DataRecord]): Stitch[Seq[Score]] = - throw new UnsupportedOperationException( - "For instances of AdhocScorer this operation is not defined. Please use " + - "`def score(target: HasClientContext with HasParams, candidates: Seq[CandidateUser])` " + - "instead.") - - /** - * This helps us manage the extend of adhoc modification on candidates' score. There is a hard - * limit of applying ONLY ONE scorer of each type to a score. - */ - val scoreModificationType: AdhocScoreModificationType -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/BUILD deleted file mode 100644 index bbcd3c708..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/BUILD +++ /dev/null @@ -1,23 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking:ml_ranker_params", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "src/java/com/twitter/ml/api:api-base", - "src/scala/com/twitter/pluck/source/core_workflows/user_model:condensed_user_state-scala", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/DeepbirdScorer.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/DeepbirdScorer.scala deleted file mode 100644 index d27bc6e37..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/DeepbirdScorer.scala +++ /dev/null @@ -1,151 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.ml_ranker.scoring - -import com.twitter.cortex.deepbird.thriftjava.DeepbirdPredictionService -import com.twitter.cortex.deepbird.thriftjava.ModelSelector -import com.twitter.finagle.stats.Stat -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDebugOptions -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.Score -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.Feature -import com.twitter.ml.api.RichDataRecord -import com.twitter.ml.prediction_service.{BatchPredictionRequest => JBatchPredictionRequest} -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.Future -import com.twitter.util.TimeoutException -import scala.collection.JavaConversions._ -import scala.collection.JavaConverters._ - -/** - * Generic trait that implements the scoring given a deepbirdClient - * To test out a new model, create a scorer extending this trait, override the modelName and inject the scorer - */ -trait DeepbirdScorer extends Scorer { - def modelName: String - def predictionFeature: Feature.Continuous - // Set a default batchSize of 100 when making model prediction calls to the Deepbird V2 prediction server - def batchSize: Int = 100 - def deepbirdClient: DeepbirdPredictionService.ServiceToClient - def baseStats: StatsReceiver - - def modelSelector: ModelSelector = new ModelSelector().setId(modelName) - def stats: StatsReceiver = baseStats.scope(this.getClass.getSimpleName).scope(modelName) - - private def requestCount = stats.counter("requests") - private def emptyRequestCount = stats.counter("empty_requests") - private def successCount = stats.counter("success") - private def failureCount = stats.counter("failures") - private def inputRecordsStat = stats.stat("input_records") - private def outputRecordsStat = stats.stat("output_records") - - // Counters for tracking batch-prediction statistics when making DBv2 prediction calls - // - // numBatchRequests tracks the number of batch prediction requests made to DBv2 prediction servers - private def numBatchRequests = stats.counter("batches") - // numEmptyBatchRequests tracks the number of batch prediction requests made to DBv2 prediction servers - // that had an empty input DataRecord - private def numEmptyBatchRequests = stats.counter("empty_batches") - // numTimedOutBatchRequests tracks the number of batch prediction requests made to DBv2 prediction servers - // that had timed-out - private def numTimedOutBatchRequests = stats.counter("timeout_batches") - - private def batchPredictionLatency = stats.stat("batch_prediction_latency") - private def predictionLatency = stats.stat("prediction_latency") - - private def numEmptyModelPredictions = stats.counter("empty_model_predictions") - private def numNonEmptyModelPredictions = stats.counter("non_empty_model_predictions") - - private val DefaultPredictionScore = 0.0 - - /** - * NOTE: For instances of [[DeepbirdScorer]] this function SHOULD NOT be used. - * Please use [[score(records: Seq[DataRecord])]] instead. - */ - @Deprecated - def score( - target: HasClientContext with HasParams with HasDisplayLocation with HasDebugOptions, - candidates: Seq[CandidateUser] - ): Seq[Option[Score]] = - throw new UnsupportedOperationException( - "For instances of DeepbirdScorer this operation is not defined. Please use " + - "`def score(records: Seq[DataRecord]): Stitch[Seq[Score]]` " + - "instead.") - - override def score(records: Seq[DataRecord]): Stitch[Seq[Score]] = { - requestCount.incr() - if (records.isEmpty) { - emptyRequestCount.incr() - Stitch.Nil - } else { - inputRecordsStat.add(records.size) - Stitch.callFuture( - batchPredict(records, batchSize) - .map { recordList => - val scores = recordList.map { record => - Score( - value = record.getOrElse(DefaultPredictionScore), - rankerId = Some(id), - scoreType = scoreType) - } - outputRecordsStat.add(scores.size) - scores - }.onSuccess(_ => successCount.incr()) - .onFailure(_ => failureCount.incr())) - } - } - - def batchPredict( - dataRecords: Seq[DataRecord], - batchSize: Int - ): Future[Seq[Option[Double]]] = { - Stat - .timeFuture(predictionLatency) { - val batchedDataRecords = dataRecords.grouped(batchSize).toSeq - numBatchRequests.incr(batchedDataRecords.size) - Future - .collect(batchedDataRecords.map(batch => predict(batch))) - .map(res => res.reduce(_ ++ _)) - } - } - - def predict(dataRecords: Seq[DataRecord]): Future[Seq[Option[Double]]] = { - Stat - .timeFuture(batchPredictionLatency) { - if (dataRecords.isEmpty) { - numEmptyBatchRequests.incr() - Future.Nil - } else { - deepbirdClient - .batchPredictFromModel(new JBatchPredictionRequest(dataRecords.asJava), modelSelector) - .map { response => - response.predictions.toSeq.map { prediction => - val predictionFeatureOption = Option( - new RichDataRecord(prediction).getFeatureValue(predictionFeature) - ) - predictionFeatureOption match { - case Some(predictionValue) => - numNonEmptyModelPredictions.incr() - Option(predictionValue.toDouble) - case None => - numEmptyModelPredictions.incr() - Option(DefaultPredictionScore) - } - } - } - .rescue { - case e: TimeoutException => // DBv2 prediction calls that timed out - numTimedOutBatchRequests.incr() - stats.counter(e.getClass.getSimpleName).incr() - Future.value(dataRecords.map(_ => Option(DefaultPredictionScore))) - case e: Exception => // other generic DBv2 prediction call failures - stats.counter(e.getClass.getSimpleName).incr() - Future.value(dataRecords.map(_ => Option(DefaultPredictionScore))) - } - } - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/PostnuxDeepbirdProdScorer.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/PostnuxDeepbirdProdScorer.scala deleted file mode 100644 index 861d02fa7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/PostnuxDeepbirdProdScorer.scala +++ /dev/null @@ -1,34 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.ml_ranker.scoring - -import com.twitter.cortex.deepbird.thriftjava.DeepbirdPredictionService -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.follow_recommendations.common.rankers.common.RankerId -import com.twitter.ml.api.Feature -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -// This is a standard DeepbirdV2 ML Ranker scoring config that should be extended by all ML scorers -// -// Only modify this trait when adding new fields to DeepbirdV2 scorers which -trait DeepbirdProdScorer extends DeepbirdScorer { - override val batchSize = 20 -} - -// Feature.Continuous("prediction") is specific to ClemNet architecture, we can change it to be more informative in the next iteration -trait PostNuxV1DeepbirdProdScorer extends DeepbirdProdScorer { - override val predictionFeature: Feature.Continuous = - new Feature.Continuous("prediction") -} - -// The current, primary PostNUX DeepbirdV2 scorer used in production -@Singleton -class PostnuxDeepbirdProdScorer @Inject() ( - @Named(GuiceNamedConstants.WTF_PROD_DEEPBIRDV2_CLIENT) - override val deepbirdClient: DeepbirdPredictionService.ServiceToClient, - override val baseStats: StatsReceiver) - extends PostNuxV1DeepbirdProdScorer { - override val id = RankerId.PostNuxProdRanker - override val modelName = "PostNUX14531GafClemNetWarmStart" -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/RandomScorer.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/RandomScorer.scala deleted file mode 100644 index 92265cc6b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/RandomScorer.scala +++ /dev/null @@ -1,42 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.ml_ranker.scoring - -import com.twitter.cortex.deepbird.thriftjava.DeepbirdPredictionService -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.follow_recommendations.common.rankers.common.RankerId -import com.twitter.ml.api.DataRecord -import com.twitter.ml.api.Feature -import com.twitter.util.Future -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -/** - * This scorer assigns random values between 0 and 1 to each candidate as scores. - */ - -@Singleton -class RandomScorer @Inject() ( - @Named(GuiceNamedConstants.WTF_PROD_DEEPBIRDV2_CLIENT) - override val deepbirdClient: DeepbirdPredictionService.ServiceToClient, - override val baseStats: StatsReceiver) - extends DeepbirdScorer { - override val id = RankerId.RandomRanker - private val rnd = new scala.util.Random(System.currentTimeMillis()) - - override def predict(dataRecords: Seq[DataRecord]): Future[Seq[Option[Double]]] = { - if (dataRecords.isEmpty) { - Future.Nil - } else { - // All candidates are assigned a random value between 0 and 1 as score. - Future.value(dataRecords.map(_ => Option(rnd.nextDouble()))) - } - } - - override val modelName = "PostNuxRandomRanker" - - // This is not needed since we are overriding the `predict` function, but we have to override - // `predictionFeature` anyway. - override val predictionFeature: Feature.Continuous = - new Feature.Continuous("prediction.pfollow_pengagement") -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/Scorer.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/Scorer.scala deleted file mode 100644 index 2ca611535..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/Scorer.scala +++ /dev/null @@ -1,34 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.ml_ranker.scoring - -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasDebugOptions -import com.twitter.follow_recommendations.common.models.Score -import com.twitter.follow_recommendations.common.models.ScoreType -import com.twitter.follow_recommendations.common.rankers.common.RankerId -import com.twitter.ml.api.DataRecord -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -trait Scorer { - - // unique id of the scorer - def id: RankerId.Value - - // type of the output scores - def scoreType: Option[ScoreType] = None - - // Scoring when an ML model is used. - def score(records: Seq[DataRecord]): Stitch[Seq[Score]] - - /** - * Scoring when a non-ML method is applied. E.g: Boosting, randomized reordering, etc. - * This method assumes that candidates' scores are already retrieved from heavy-ranker models and - * are available for use. - */ - def score( - target: HasClientContext with HasParams with HasDisplayLocation with HasDebugOptions, - candidates: Seq[CandidateUser] - ): Seq[Option[Score]] -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/ScorerFactory.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/ScorerFactory.scala deleted file mode 100644 index a9ea0a21b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring/ScorerFactory.scala +++ /dev/null @@ -1,38 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.ml_ranker.scoring - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.rankers.common.RankerId -import com.twitter.follow_recommendations.common.rankers.common.RankerId.RankerId -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ScorerFactory @Inject() ( - postnuxProdScorer: PostnuxDeepbirdProdScorer, - randomScorer: RandomScorer, - stats: StatsReceiver) { - - private val scorerFactoryStats = stats.scope("scorer_factory") - private val scorerStat = scorerFactoryStats.scope("scorer") - - def getScorers( - rankerIds: Seq[RankerId] - ): Seq[Scorer] = { - rankerIds.map { scorerId => - val scorer: Scorer = getScorerById(scorerId) - // count # of times a ranker has been requested - scorerStat.counter(scorer.id.toString).incr() - scorer - } - } - - def getScorerById(scorerId: RankerId): Scorer = scorerId match { - case RankerId.PostNuxProdRanker => - postnuxProdScorer - case RankerId.RandomRanker => - randomScorer - case _ => - scorerStat.counter("invalid_scorer_type").incr() - throw new IllegalArgumentException("unknown_scorer_type") - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils/BUILD deleted file mode 100644 index 82e9fc7f4..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils/BUILD +++ /dev/null @@ -1,8 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils/Utils.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils/Utils.scala deleted file mode 100644 index 29f00b698..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils/Utils.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.utils - -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.Score -import com.twitter.follow_recommendations.common.rankers.common.RankerId.RankerId - -object Utils { - - /** - * Add the ranking and scoring info for a list of candidates on a given ranking stage. - * @param candidates A list of CandidateUser - * @param rankingStage Should use `Ranker.name` as the ranking stage. - * @return The list of CandidateUser with ranking/scoring info added. - */ - def addRankingInfo(candidates: Seq[CandidateUser], rankingStage: String): Seq[CandidateUser] = { - candidates.zipWithIndex.map { - case (candidate, rank) => - // 1-based ranking for better readability - candidate.addInfoPerRankingStage(rankingStage, candidate.scores, rank + 1) - } - } - - def getCandidateScoreByRankerId(candidate: CandidateUser, rankerId: RankerId): Option[Score] = - candidate.scores.flatMap { ss => ss.scores.find(_.rankerId.contains(rankerId)) } - - def getAllRankerIds(candidates: Seq[CandidateUser]): Seq[RankerId] = - candidates.flatMap(_.scores.map(_.scores.flatMap(_.rankerId))).flatten.distinct -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/BUILD deleted file mode 100644 index 3de5523b1..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/CandidateShuffle.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/CandidateShuffle.scala deleted file mode 100644 index be281a582..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/CandidateShuffle.scala +++ /dev/null @@ -1,36 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker - -import com.twitter.follow_recommendations.common.utils.RandomUtil -import scala.util.Random - -sealed trait CandidateShuffler[T] { - def shuffle(seed: Option[Long])(input: Seq[T]): Seq[T] -} - -class NoShuffle[T]() extends CandidateShuffler[T] { - def shuffle(seed: Option[Long])(input: Seq[T]): Seq[T] = input -} - -class RandomShuffler[T]() extends CandidateShuffler[T] { - def shuffle(seed: Option[Long])(input: Seq[T]): Seq[T] = { - seed.map(new Random(_)).getOrElse(Random).shuffle(input) - } -} - -trait RankWeightedRandomShuffler[T] extends CandidateShuffler[T] { - - def rankToWeight(rank: Int): Double - def shuffle(seed: Option[Long])(input: Seq[T]): Seq[T] = { - val candWeights = input.zipWithIndex.map { - case (candidate, rank) => (candidate, rankToWeight(rank)) - } - RandomUtil.weightedRandomShuffle(candWeights, seed.map(new Random(_))).unzip._1 - } -} - -class ExponentialShuffler[T]() extends RankWeightedRandomShuffler[T] { - def rankToWeight(rank: Int): Double = { - 1 / math - .pow(rank.toDouble, 2.0) // this function was proved to be effective in previous DDGs - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightMethod.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightMethod.scala deleted file mode 100644 index 54e2ad549..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightMethod.scala +++ /dev/null @@ -1,6 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker - -object WeightMethod extends Enumeration { - type WeightMethod = Value - val WeightedRandomSampling, WeightedRoundRobin = Value -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceBaseRanker.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceBaseRanker.scala deleted file mode 100644 index e348560db..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceBaseRanker.scala +++ /dev/null @@ -1,118 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker - -import com.twitter.follow_recommendations.common.utils.RandomUtil -import com.twitter.follow_recommendations.common.utils.MergeUtil -import com.twitter.follow_recommendations.common.utils.Weighted -import com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker.WeightMethod._ -import scala.util.Random - -/** - * This ranker selects the next candidate source to select a candidate from. It supports - * two kinds of algorithm, WeightedRandomSampling or WeightedRoundRobin. WeightedRandomSampling - * pick the next candidate source randomly, WeightedRoundRobin picked the next candidate source - * sequentially based on the weight of the candidate source. It is default to WeightedRandomSampling - * if no weight method is provided. - * - * Example usage of this class: - * - * When use WeightedRandomSampling: - * Input candidate sources and their weights are: {{CS1: 3}, {CS2: 2}, {CS3: 5}} - * Ranked candidates sequence is not determined because of random sampling. - * One possible output candidate sequence is: (CS1_candidate1, CS2_candidate1, CS2_candidate2, - * CS3_candidate1, CS3_candidates2, CS3_candidate3, CS1_candidate2, CS1_candidate3, - * CS3_candidate4, CS3_candidate5, CS1_candidate4, CS1_candidate5, CS2_candidate6, CS2_candidate3,...) - * - * When use WeightedRoundRobin: - * Input candidate sources and their weights are: {{CS1: 3}, {CS2: 2}, {CS3: 5}} - * Output candidate sequence is: (CS1_candidate1, CS1_candidate2, CS1_candidate3, - * CS2_candidate1, CS2_candidates2, CS3_candidate1, CS3_candidate2, CS3_candidate3, - * CS3_candidate4, CS3_candidate5, CS1_candidate4, CS1_candidate5, CS1_candidate6, CS2_candidate3,...) - * - * Note: CS1_candidate1 means the first candidate in CS1 candidate source. - * - * @tparam A candidate source type - * @tparam Rec Recommendation type - * @param candidateSourceWeights relative weights for different candidate sources - */ -class WeightedCandidateSourceBaseRanker[A, Rec]( - candidateSourceWeights: Map[A, Double], - weightMethod: WeightMethod = WeightedRandomSampling, - randomSeed: Option[Long]) { - - /** - * Creates a iterator over algorithms and calls next to return a Stream of candidates - * - * - * @param candidateSources the set of candidate sources that are being sampled - * @param candidateSourceWeights map of candidate source to weight - * @param candidates the map of candidate source to the iterator of its results - * @param weightMethod a enum to indict which weight method to use. Two values are supported - * currently. When WeightedRandomSampling is set, the next candidate is picked from a candidate - * source that is randomly chosen. When WeightedRoundRobin is set, the next candidate is picked - * from current candidate source until the number of candidates reaches to the assigned weight of - * the candidate source. The next call of this function will return a candidate from the next - * candidate source which is after previous candidate source based on the order input - * candidate source sequence. - - * @return stream of candidates - */ - def stream( - candidateSources: Set[A], - candidateSourceWeights: Map[A, Double], - candidates: Map[A, Iterator[Rec]], - weightMethod: WeightMethod = WeightedRandomSampling, - random: Option[Random] = None - ): Stream[Rec] = { - val weightedCandidateSource: Weighted[A] = new Weighted[A] { - override def apply(a: A): Double = candidateSourceWeights.getOrElse(a, 0) - } - - /** - * Generates a stream of candidates. - * - * @param candidateSourceIter an iterator over candidate sources returned by the sampling procedure - * @return stream of candidates - */ - def next(candidateSourceIter: Iterator[A]): Stream[Rec] = { - val source = candidateSourceIter.next() - val it = candidates(source) - if (it.hasNext) { - val currCand = it.next() - currCand #:: next(candidateSourceIter) - } else { - assert(candidateSources.contains(source), "Selected source is not in candidate sources") - // Remove the depleted candidate source and re-sample - stream(candidateSources - source, candidateSourceWeights, candidates, weightMethod, random) - } - } - if (candidateSources.isEmpty) - Stream.empty - else { - val candidateSourceSeq = candidateSources.toSeq - val candidateSourceIter = - if (weightMethod == WeightMethod.WeightedRoundRobin) { - MergeUtil.weightedRoundRobin(candidateSourceSeq)(weightedCandidateSource).iterator - } else { - //default to weighted random sampling if no other weight method is provided - RandomUtil - .weightedRandomSamplingWithReplacement( - candidateSourceSeq, - random - )(weightedCandidateSource).iterator - } - next(candidateSourceIter) - } - } - - def apply(input: Map[A, TraversableOnce[Rec]]): Stream[Rec] = { - stream( - input.keySet, - candidateSourceWeights, - input.map { - case (k, v) => k -> v.toIterator - }, // cannot do mapValues here, as that only returns a view - weightMethod, - randomSeed.map(new Random(_)) - ) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRanker.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRanker.scala deleted file mode 100644 index c6f55adbc..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRanker.scala +++ /dev/null @@ -1,100 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker -import com.twitter.follow_recommendations.common.base.Ranker -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.rankers.common.DedupCandidates -import com.twitter.follow_recommendations.common.rankers.utils.Utils -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -/** - * Candidate Ranker that mixes and ranks multiple candidate lists from different candidate sources with the - * following steps: - * 1) generate a ranked candidate list of each candidate source by sorting and shuffling the candidate list - * of the algorithm. - * 2) merge the ranked lists generated in 1) into a single list using weighted randomly sampling. - * 3) If dedup is required, dedup the output from 2) by candidate id. - * - * @param basedRanker base ranker - * @param shuffleFn the shuffle function that will be used to shuffle each algorithm's sorted candidate list. - * @param dedup whether to remove duplicated candidates from the final output. - */ -class WeightedCandidateSourceRanker[Target <: HasParams]( - basedRanker: WeightedCandidateSourceBaseRanker[ - CandidateSourceIdentifier, - CandidateUser - ], - shuffleFn: Seq[CandidateUser] => Seq[CandidateUser], - dedup: Boolean) - extends Ranker[Target, CandidateUser] { - - val name: String = this.getClass.getSimpleName - - override def rank(target: Target, candidates: Seq[CandidateUser]): Stitch[Seq[CandidateUser]] = { - val scribeRankingInfo: Boolean = - target.params(WeightedCandidateSourceRankerParams.ScribeRankingInfoInWeightedRanker) - val rankedCands = rankCandidates(group(candidates)) - Stitch.value(if (scribeRankingInfo) Utils.addRankingInfo(rankedCands, name) else rankedCands) - } - - private def group( - candidates: Seq[CandidateUser] - ): Map[CandidateSourceIdentifier, Seq[CandidateUser]] = { - val flattened = for { - candidate <- candidates - identifier <- candidate.getPrimaryCandidateSource - } yield (identifier, candidate) - flattened.groupBy(_._1).mapValues(_.map(_._2)) - } - - private def rankCandidates( - input: Map[CandidateSourceIdentifier, Seq[CandidateUser]] - ): Seq[CandidateUser] = { - // Sort and shuffle candidates per candidate source. - // Note 1: Using map instead mapValue here since mapValue somehow caused infinite loop when used as part of Stream. - val sortAndShuffledCandidates = input.map { - case (source, candidates) => - // Note 2: toList is required here since candidates is a view, and it will result in infinit loop when used as part of Stream. - // Note 3: there is no real sorting logic here, it assumes the input is already sorted by candidate sources - val sortedCandidates = candidates.toList - source -> shuffleFn(sortedCandidates).iterator - } - val rankedCandidates = basedRanker(sortAndShuffledCandidates) - - if (dedup) DedupCandidates(rankedCandidates) else rankedCandidates - } -} - -object WeightedCandidateSourceRanker { - - def build[Target <: HasParams]( - candidateSourceWeight: Map[CandidateSourceIdentifier, Double], - shuffleFn: Seq[CandidateUser] => Seq[CandidateUser] = identity, - dedup: Boolean = false, - randomSeed: Option[Long] = None - ): WeightedCandidateSourceRanker[Target] = { - new WeightedCandidateSourceRanker( - new WeightedCandidateSourceBaseRanker( - candidateSourceWeight, - WeightMethod.WeightedRandomSampling, - randomSeed = randomSeed), - shuffleFn, - dedup - ) - } -} - -object WeightedCandidateSourceRankerWithoutRandomSampling { - def build[Target <: HasParams]( - candidateSourceWeight: Map[CandidateSourceIdentifier, Double] - ): WeightedCandidateSourceRanker[Target] = { - new WeightedCandidateSourceRanker( - new WeightedCandidateSourceBaseRanker( - candidateSourceWeight, - WeightMethod.WeightedRoundRobin, - randomSeed = None), - identity, - false, - ) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRankerFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRankerFSConfig.scala deleted file mode 100644 index 58f0fde3e..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRankerFSConfig.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSParam - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class WeightedCandidateSourceRankerFSConfig @Inject() extends FeatureSwitchConfig { - override val booleanFSParams: Seq[FSParam[Boolean]] = - Seq(WeightedCandidateSourceRankerParams.ScribeRankingInfoInWeightedRanker) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRankerParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRankerParams.scala deleted file mode 100644 index ff4ecae4b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker/WeightedCandidateSourceRankerParams.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker - -import com.twitter.timelines.configapi.FSParam - -object WeightedCandidateSourceRankerParams { - case object ScribeRankingInfoInWeightedRanker - extends FSParam[Boolean]("weighted_ranker_scribe_ranking_info", false) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/stores/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/stores/BUILD deleted file mode 100644 index 3b7a46db7..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/stores/BUILD +++ /dev/null @@ -1,19 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "strato/config/columns/onboarding/userrecs:userrecs-strato-client", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/stores/LowTweepCredFollowStore.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/stores/LowTweepCredFollowStore.scala deleted file mode 100644 index d2f4e035b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/stores/LowTweepCredFollowStore.scala +++ /dev/null @@ -1,39 +0,0 @@ -package com.twitter.follow_recommendations.common.stores - -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.stitch.Stitch -import com.twitter.strato.generated.client.onboarding.userrecs.TweepCredOnUserClientColumn -import javax.inject.Inject -import javax.inject.Singleton - -// Not a candidate source since it's a intermediary. -@Singleton -class LowTweepCredFollowStore @Inject() (tweepCredOnUserClientColumn: TweepCredOnUserClientColumn) { - - def getLowTweepCredUsers(target: HasRecentFollowedUserIds): Stitch[Seq[CandidateUser]] = { - val newFollowings = - target.recentFollowedUserIds.getOrElse(Nil).take(LowTweepCredFollowStore.NumFlockToRetrieve) - - val validTweepScoreUserIdsStitch: Stitch[Seq[Long]] = Stitch - .traverse(newFollowings) { newFollowingUserId => - val tweepCredScoreOptStitch = tweepCredOnUserClientColumn.fetcher - .fetch(newFollowingUserId) - .map(_.v) - tweepCredScoreOptStitch.map(_.flatMap(tweepCred => - if (tweepCred < LowTweepCredFollowStore.TweepCredThreshold) { - Some(newFollowingUserId) - } else { - None - })) - }.map(_.flatten) - - validTweepScoreUserIdsStitch - .map(_.map(CandidateUser(_, Some(CandidateUser.DefaultCandidateScore)))) - } -} - -object LowTweepCredFollowStore { - val NumFlockToRetrieve = 500 - val TweepCredThreshold = 40 -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/dedup/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/dedup/BUILD deleted file mode 100644 index 35534b064..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/dedup/BUILD +++ /dev/null @@ -1,8 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/dedup/DedupTransform.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/dedup/DedupTransform.scala deleted file mode 100644 index 64f73d6ae..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/dedup/DedupTransform.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.twitter.follow_recommendations.common.transforms.dedup - -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.product_mixer.core.model.common.UniversalNoun -import com.twitter.stitch.Stitch -import scala.collection.mutable - -class DedupTransform[Request, Candidate <: UniversalNoun[Long]]() - extends Transform[Request, Candidate] { - override def transform(target: Request, candidates: Seq[Candidate]): Stitch[Seq[Candidate]] = { - val seen = mutable.HashSet[Long]() - Stitch.value(candidates.filter(candidate => seen.add(candidate.id))) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/BUILD deleted file mode 100644 index 79da9c259..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/BUILD +++ /dev/null @@ -1,22 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "configapi/configapi-core", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders", - "snowflake/src/main/scala/com/twitter/snowflake/id", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/ModifySocialProofTransform.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/ModifySocialProofTransform.scala deleted file mode 100644 index 306578a4d..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/ModifySocialProofTransform.scala +++ /dev/null @@ -1,202 +0,0 @@ -package com.twitter.follow_recommendations.common.transforms.modify_social_proof - -import com.twitter.conversions.DurationOps._ -import com.twitter.decider.Decider -import com.twitter.decider.RandomRecipient -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.finagle.util.DefaultTimer -import com.twitter.follow_recommendations.common.base.GatedTransform -import com.twitter.follow_recommendations.common.clients.graph_feature_service.GraphFeatureServiceClient -import com.twitter.follow_recommendations.common.clients.socialgraph.SocialGraphClient -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FollowProof -import com.twitter.follow_recommendations.configapi.deciders.DeciderKey -import com.twitter.graph_feature_service.thriftscala.EdgeType -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.snowflake.id.SnowflakeId -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.logging.Logging -import com.twitter.util.Duration -import com.twitter.util.Time -import javax.inject.Inject -import javax.inject.Singleton - -object ModifySocialProof { - val GfsLagDuration: Duration = 14.days - val GfsIntersectionIds: Int = 3 - val SgsIntersectionIds: Int = 10 - val LeftEdgeTypes: Set[EdgeType] = Set(EdgeType.Following) - val RightEdgeTypes: Set[EdgeType] = Set(EdgeType.FollowedBy) - - /** - * Given the intersection ID's for a particular candidate, update the candidate's social proof - * @param candidate candidate object - * @param followProof follow proof to be added (includes id's and count) - * @param stats stats for tracking - * @return updated candidate object - */ - def addIntersectionIdsToCandidate( - candidate: CandidateUser, - followProof: FollowProof, - stats: StatsReceiver - ): CandidateUser = { - // create updated set of social proof - val updatedFollowedByOpt = candidate.followedBy match { - case Some(existingFollowedBy) => Some((followProof.followedBy ++ existingFollowedBy).distinct) - case None if followProof.followedBy.nonEmpty => Some(followProof.followedBy.distinct) - case _ => None - } - - val updatedFollowProof = updatedFollowedByOpt.map { updatedFollowedBy => - val updatedCount = followProof.numIds.max(updatedFollowedBy.size) - // track stats - val numSocialProofAdded = updatedFollowedBy.size - candidate.followedBy.size - addCandidatesWithSocialContextCountStat(stats, numSocialProofAdded) - FollowProof(updatedFollowedBy, updatedCount) - } - - candidate.setFollowProof(updatedFollowProof) - } - - private def addCandidatesWithSocialContextCountStat( - statsReceiver: StatsReceiver, - count: Int - ): Unit = { - if (count > 3) { - statsReceiver.counter("4_and_more").incr() - } else { - statsReceiver.counter(count.toString).incr() - } - } -} - -/** - * This class makes a request to gfs/sgs for hydrating additional social proof on each of the - * provided candidates. - */ -@Singleton -class ModifySocialProof @Inject() ( - gfsClient: GraphFeatureServiceClient, - socialGraphClient: SocialGraphClient, - statsReceiver: StatsReceiver, - decider: Decider = Decider.True) - extends Logging { - import ModifySocialProof._ - - private val stats = statsReceiver.scope(this.getClass.getSimpleName) - private val addedStats = stats.scope("num_social_proof_added_per_candidate") - private val gfsStats = stats.scope("graph_feature_service") - private val sgsStats = stats.scope("social_graph_service") - private val previousProofEmptyCounter = stats.counter("previous_proof_empty") - private val emptyFollowProofCounter = stats.counter("empty_followed_proof") - - /** - * For each candidate provided, we get the intersectionIds between the user and the candidate, - * appending the unique results to the social proof (followedBy field) if not already previously - * seen we query GFS for all users, except for cases specified via the mustCallSgs field or for - * very new users, who would not have any data in GFS, due to the lag duration of the service's - * processing. this is determined by GfsLagDuration - * @param userId id of the target user whom we provide recommendations for - * @param candidates list of candidates - * @param intersectionIdsNum if provided, determines the maximum number of accounts we want to be hydrated for social proof - * @param mustCallSgs Determines if we should query SGS regardless of user age or not. - * @return list of candidates updated with additional social proof - */ - def hydrateSocialProof( - userId: Long, - candidates: Seq[CandidateUser], - intersectionIdsNum: Option[Int] = None, - mustCallSgs: Boolean = false, - callSgsCachedColumn: Boolean = false, - gfsLagDuration: Duration = GfsLagDuration, - gfsIntersectionIds: Int = GfsIntersectionIds, - sgsIntersectionIds: Int = SgsIntersectionIds, - ): Stitch[Seq[CandidateUser]] = { - addCandidatesWithSocialContextCountStat( - stats.scope("social_context_count_before_hydration"), - candidates.count(_.followedBy.isDefined) - ) - val candidateIds = candidates.map(_.id) - val userAgeOpt = SnowflakeId.timeFromIdOpt(userId).map(Time.now - _) - - // this decider gate is used to determine what % of requests is allowed to call - // Graph Feature Service. this is useful for ramping down requests to Graph Feature Service - // when necessary - val deciderKey: String = DeciderKey.EnableGraphFeatureServiceRequests.toString - val enableGfsRequests: Boolean = decider.isAvailable(deciderKey, Some(RandomRecipient)) - - // if new query sgs - val (candidateToIntersectionIdsMapFut, isGfs) = - if (!enableGfsRequests || mustCallSgs || userAgeOpt.exists(_ < gfsLagDuration)) { - ( - if (callSgsCachedColumn) - socialGraphClient.getIntersectionsFromCachedColumn( - userId, - candidateIds, - intersectionIdsNum.getOrElse(sgsIntersectionIds) - ) - else - socialGraphClient.getIntersections( - userId, - candidateIds, - intersectionIdsNum.getOrElse(sgsIntersectionIds)), - false) - } else { - ( - gfsClient.getIntersections( - userId, - candidateIds, - intersectionIdsNum.getOrElse(gfsIntersectionIds)), - true) - } - val finalCandidates = candidateToIntersectionIdsMapFut - .map { candidateToIntersectionIdsMap => - { - previousProofEmptyCounter.incr(candidates.count(_.followedBy.exists(_.isEmpty))) - candidates.map { candidate => - addIntersectionIdsToCandidate( - candidate, - candidateToIntersectionIdsMap.getOrElse(candidate.id, FollowProof(Seq.empty, 0)), - addedStats) - } - } - } - .within(250.milliseconds)(DefaultTimer) - .rescue { - case e: Exception => - error(e.getMessage) - if (isGfs) { - gfsStats.scope("rescued").counter(e.getClass.getSimpleName).incr() - } else { - sgsStats.scope("rescued").counter(e.getClass.getSimpleName).incr() - } - Stitch.value(candidates) - } - - finalCandidates.onSuccess { candidatesSeq => - emptyFollowProofCounter.incr(candidatesSeq.count(_.followedBy.exists(_.isEmpty))) - addCandidatesWithSocialContextCountStat( - stats.scope("social_context_count_after_hydration"), - candidatesSeq.count(_.followedBy.isDefined) - ) - } - } -} - -/** - * This transform uses ModifySocialProof (which makes a request to gfs/sgs) for hydrating additional - * social proof on each of the provided candidates. - */ -@Singleton -class ModifySocialProofTransform @Inject() (modifySocialProof: ModifySocialProof) - extends GatedTransform[HasClientContext with HasParams, CandidateUser] - with Logging { - - override def transform( - target: HasClientContext with HasParams, - candidates: Seq[CandidateUser] - ): Stitch[Seq[CandidateUser]] = - target.getOptionalUserId - .map(modifySocialProof.hydrateSocialProof(_, candidates)).getOrElse(Stitch.value(candidates)) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/RemoveAccountProofTransform.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/RemoveAccountProofTransform.scala deleted file mode 100644 index 8face1164..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof/RemoveAccountProofTransform.scala +++ /dev/null @@ -1,27 +0,0 @@ -package com.twitter.follow_recommendations.common.transforms.modify_social_proof - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.GatedTransform -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RemoveAccountProofTransform @Inject() (statsReceiver: StatsReceiver) - extends GatedTransform[HasClientContext with HasParams, CandidateUser] { - - private val stats = statsReceiver.scope(this.getClass.getSimpleName) - private val removedProofsCounter = stats.counter("num_removed_proofs") - - override def transform( - target: HasClientContext with HasParams, - items: Seq[CandidateUser] - ): Stitch[Seq[CandidateUser]] = - Stitch.value(items.map { candidate => - removedProofsCounter.incr() - candidate.copy(reason = None) - }) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/ranker_id/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/ranker_id/BUILD deleted file mode 100644 index d6dcd8522..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/ranker_id/BUILD +++ /dev/null @@ -1,19 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "hermit/hermit-core/src/main/scala/com/twitter/hermit/constants", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/ranker_id/RandomRankerIdTransform.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/ranker_id/RandomRankerIdTransform.scala deleted file mode 100644 index 03639da26..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/ranker_id/RandomRankerIdTransform.scala +++ /dev/null @@ -1,24 +0,0 @@ -package com.twitter.follow_recommendations.common.transforms.ranker_id - -import com.google.inject.Inject -import com.google.inject.Singleton -import com.twitter.follow_recommendations.common.base.GatedTransform -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.Score -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams - -/** - * This class appends each candidate's rankerIds with the RandomRankerId. - * This is primarily for determining if a candidate was generated via random shuffling. - */ -@Singleton -class RandomRankerIdTransform @Inject() () extends GatedTransform[HasParams, CandidateUser] { - - override def transform( - target: HasParams, - candidates: Seq[CandidateUser] - ): Stitch[Seq[CandidateUser]] = { - Stitch.value(candidates.map(_.addScore(Score.RandomScore))) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/recommendation_flow_identifier/AddRecommendationFlowIdentifierTransform.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/recommendation_flow_identifier/AddRecommendationFlowIdentifierTransform.scala deleted file mode 100644 index 87c111a6c..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/recommendation_flow_identifier/AddRecommendationFlowIdentifierTransform.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.twitter.follow_recommendations.common.transforms.recommendation_flow_identifier - -import com.google.inject.Inject -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasRecommendationFlowIdentifier -import com.twitter.stitch.Stitch - -class AddRecommendationFlowIdentifierTransform @Inject() - extends Transform[HasRecommendationFlowIdentifier, CandidateUser] { - - override def transform( - target: HasRecommendationFlowIdentifier, - items: Seq[CandidateUser] - ): Stitch[Seq[CandidateUser]] = { - Stitch.value(items.map { candidateUser => - candidateUser.copy(recommendationFlowIdentifier = target.recommendationFlowIdentifier) - }) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/recommendation_flow_identifier/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/recommendation_flow_identifier/BUILD deleted file mode 100644 index 820e2df66..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/recommendation_flow_identifier/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/tracking_token/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/tracking_token/BUILD deleted file mode 100644 index d9b257348..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/tracking_token/BUILD +++ /dev/null @@ -1,18 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "hermit/hermit-core/src/main/scala/com/twitter/hermit/constants", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/tracking_token/TrackingTokenTransform.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/tracking_token/TrackingTokenTransform.scala deleted file mode 100644 index 5a30c9cb1..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/tracking_token/TrackingTokenTransform.scala +++ /dev/null @@ -1,76 +0,0 @@ -package com.twitter.follow_recommendations.common.transforms.tracking_token - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.Session -import com.twitter.follow_recommendations.common.models.TrackingToken -import com.twitter.hermit.constants.AlgorithmFeedbackTokens.AlgorithmToFeedbackTokenMap -import com.twitter.hermit.model.Algorithm -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.stitch.Stitch -import com.twitter.util.logging.Logging - -import javax.inject.Inject -import javax.inject.Singleton - -/** - * This transform adds the tracking token for all candidates - * Since this happens in the same request, we use the same trace-id for all candidates - * There are no RPC calls in this transform so it's safe to chain it with `andThen` at the end of - * all other product-specific transforms - */ -@Singleton -class TrackingTokenTransform @Inject() (baseStatsReceiver: StatsReceiver) - extends Transform[HasDisplayLocation with HasClientContext, CandidateUser] - with Logging { - - def profileResults( - target: HasDisplayLocation with HasClientContext, - candidates: Seq[CandidateUser] - ) = { - // Metrics to track # results per candidate source - val stats = baseStatsReceiver.scope(target.displayLocation.toString + "/final_results") - stats.stat("total").add(candidates.size) - - stats.counter(target.displayLocation.toString).incr() - - val flattenedCandidates: Seq[(CandidateSourceIdentifier, CandidateUser)] = for { - candidate <- candidates - identifier <- candidate.getPrimaryCandidateSource - } yield (identifier, candidate) - val candidatesGroupedBySource: Map[CandidateSourceIdentifier, Seq[CandidateUser]] = - flattenedCandidates.groupBy(_._1).mapValues(_.map(_._2)) - candidatesGroupedBySource map { - case (source, candidates) => stats.stat(source.name).add(candidates.size) - } - } - - override def transform( - target: HasDisplayLocation with HasClientContext, - candidates: Seq[CandidateUser] - ): Stitch[Seq[CandidateUser]] = { - profileResults(target, candidates) - - Stitch.value( - target.getOptionalUserId - .map { _ => - candidates.map { - candidate => - val token = Some(TrackingToken( - sessionId = Session.getSessionId, - displayLocation = Some(target.displayLocation), - controllerData = None, - algorithmId = candidate.userCandidateSourceDetails.flatMap(_.primaryCandidateSource - .flatMap { identifier => - Algorithm.withNameOpt(identifier.name).flatMap(AlgorithmToFeedbackTokenMap.get) - }) - )) - candidate.copy(trackingToken = token) - } - }.getOrElse(candidates)) - - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/BUILD deleted file mode 100644 index 606e8edfa..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/BUILD +++ /dev/null @@ -1,10 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/utils", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransform.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransform.scala deleted file mode 100644 index 269a39a48..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransform.scala +++ /dev/null @@ -1,138 +0,0 @@ -package com.twitter.follow_recommendations.common.transforms.weighted_sampling -import com.twitter.follow_recommendations.common.base.GatedTransform -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDebugOptions -import com.twitter.follow_recommendations.common.models.Score -import com.twitter.follow_recommendations.common.models.Scores -import com.twitter.follow_recommendations.common.rankers.common.RankerId -import com.twitter.follow_recommendations.common.rankers.utils.Utils -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class SamplingTransform @Inject() () - extends GatedTransform[HasClientContext with HasParams with HasDebugOptions, CandidateUser] { - - val name: String = this.getClass.getSimpleName - - /* - Description: This function takes in a set of candidate users and ranks them for a who-to-follow - request by sampling from the Placket-Luce distribution - (https://cran.rstudio.com/web/packages/PlackettLuce/vignettes/Overview.html) with a three - variations. The first variation is that the scores of the candidates are multiplied by - multiplicativeFactor before sampling. The second variation is that the scores are - exponentiated before sampling. The third variation is that depending on how many who-to-follow - positions are being requested, the first k positions are reserved for the candidates with the - highest scores (and they are sorted in decreasing order of score) and the remaining positions - are sampled from a Placket-Luce. We use the efficient algorithm proposed in this blog - https://medium.com/swlh/going-old-school-designing-algorithms-for-fast-weighted-sampling-in-production-c48fc1f40051 - to sample from a Plackett-Luce. Because of numerical stability reasons, before sampling from this - distribution, (1) we subtract off the maximum score from all the scores and (2) if after - this subtraction and multiplication by the multiplicative factor the resulting score is <= -10, - we force the candidate's transformed score under the above algorithm to be 0 (so r^(1/w) = 0) - where r is a random number and w is the transformed score. - - inputs: - - target: HasClientContext (WTF request) - - candidates: sequence of CandidateUsers (users that need to be ranked from a who-to-follow - request) each of which has a score - - inputs accessed through feature switches, i.e. through target.params (see the following file: - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/ - transforms/weighted_sampling/SamplingTransformParams.scala"): - - topKFixed: the first k positions of the who-to-follow ranking correspond to the users with the k - highest scores and are not sampled from the Placket-Luce distribution - - multiplicativeFactor: multiplicativeFactor is used to transform the scores of each candidate by - multiplying that user's score by multiplicativeFactor - - output: - - Sequence of CandidateUser whose order represents the ranking of users in a who-to-follow request - This ranking is sampled from a Placket-Luce distribution. - */ - override def transform( - target: HasClientContext with HasParams with HasDebugOptions, - candidates: Seq[CandidateUser] - ): Stitch[Seq[CandidateUser]] = { - - // the first k positions of the who-to-follow ranking correspond to the users with the k - // highest scores and are not sampled from the Placket-Luce distribution - val topKFixed = target.params(SamplingTransformParams.TopKFixed) - - // multiplicativeFactor is used to transform the scores of each candidate by - // multiplying that user's score by multiplicativeFactor - val multiplicativeFactor = target.params(SamplingTransformParams.MultiplicativeFactor) - - // sort candidates by their score - val candidatesSorted = candidates.sortBy(-1 * _.score.getOrElse(0.0)) - - // pick the top K candidates by score and the remaining candidates - val (topKFixedCandidates, candidatesOutsideOfTopK) = - candidatesSorted.zipWithIndex.partition { case (value, index) => index < topKFixed } - - val randomNumGenerator = - new scala.util.Random(target.getRandomizationSeed.getOrElse(System.currentTimeMillis)) - - // we need to subtract the maximum score off the scores for numerical stability reasons - // subtracting the max score off does not effect the underlying distribution we are sampling - // the candidates from - // we need the if statement since you cannot take the max of an empty sequence - val maximum_score = if (candidatesOutsideOfTopK.nonEmpty) { - candidatesOutsideOfTopK.map(x => x._1.score.getOrElse(0.0)).max - } else { - 0.0 - } - - // for candidates in candidatesOutsideOfTopK, we transform their score by subtracting off - // maximum_score and then multiply by multiplicativeFactor - val candidatesOutsideOfTopKTransformedScore = candidatesOutsideOfTopK.map(x => - (x._1, multiplicativeFactor * (x._1.score.getOrElse(0.0) - maximum_score))) - - // for each candidate with score transformed and clip score w, sample a random number r, - // create a new score r^(1/w) and sort the candidates to get the final ranking. - // for numerical stability reasons if the score is <=-10, we force r^(1/w) = 0. - // this samples the candidates from the modified Plackett-Luce distribution. See - // https://medium.com/swlh/going-old-school-designing-algorithms-for-fast-weighted-sampling-in-production-c48fc1f40051 - - val candidatesOutsideOfTopKSampled = candidatesOutsideOfTopKTransformedScore - .map(x => - ( - x._1, - if (x._2 <= -10.0) - 0.0 - else - scala.math.pow( - randomNumGenerator.nextFloat(), - 1 / (scala.math - .exp(x._2))))).sortBy(-1 * _._2) - - val topKCandidates: Seq[CandidateUser] = topKFixedCandidates.map(_._1) - - val scribeRankingInfo: Boolean = - target.params(SamplingTransformParams.ScribeRankingInfoInSamplingTransform) - - val transformedCandidates: Seq[CandidateUser] = if (scribeRankingInfo) { - val topKCandidatesWithRankingInfo: Seq[CandidateUser] = - Utils.addRankingInfo(topKCandidates, name) - val candidatesOutsideOfTopKSampledWithRankingInfo: Seq[CandidateUser] = - candidatesOutsideOfTopKSampled.zipWithIndex.map { - case ((candidate, score), rank) => - val newScore = Seq(Score(score, Some(RankerId.PlacketLuceSamplingTransformer))) - val newScores: Option[Scores] = candidate.scores - .map { scores => - scores.copy(scores = scores.scores ++ newScore) - }.orElse(Some(Scores(newScore, Some(RankerId.PlacketLuceSamplingTransformer)))) - val globalRank = rank + topKFixed + 1 - candidate.addInfoPerRankingStage(name, newScores, globalRank) - } - - topKCandidatesWithRankingInfo ++ candidatesOutsideOfTopKSampledWithRankingInfo - } else { - topKCandidates ++ candidatesOutsideOfTopKSampled.map(_._1) - } - - Stitch.value(transformedCandidates) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransformFSConfig.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransformFSConfig.scala deleted file mode 100644 index b97251f93..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransformFSConfig.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.twitter.follow_recommendations.common.transforms.weighted_sampling - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class SamplingTransformFSConfig @Inject() () extends FeatureSwitchConfig { - override val intFSParams: Seq[FSBoundedParam[Int]] = Seq(SamplingTransformParams.TopKFixed) - - override val doubleFSParams: Seq[FSBoundedParam[Double]] = Seq( - SamplingTransformParams.MultiplicativeFactor) - - override val booleanFSParams: Seq[FSParam[Boolean]] = Seq( - SamplingTransformParams.ScribeRankingInfoInSamplingTransform) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransformParams.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransformParams.scala deleted file mode 100644 index 363487a9b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling/SamplingTransformParams.scala +++ /dev/null @@ -1,25 +0,0 @@ -package com.twitter.follow_recommendations.common.transforms.weighted_sampling - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam - -object SamplingTransformParams { - - case object TopKFixed // indicates how many of the fisrt K who-to-follow recommendations are reserved for the candidates with largest K CandidateUser.score where these candidates are sorted in decreasing order of score - extends FSBoundedParam[Int]( - name = "post_nux_ml_flow_weighted_sampling_top_k_fixed", - default = 0, - min = 0, - max = 100) - - case object MultiplicativeFactor // CandidateUser.score gets transformed to multiplicativeFactor*CandidateUser.score before sampling from the Plackett-Luce distribution - extends FSBoundedParam[Double]( - name = "post_nux_ml_flow_weighted_sampling_multiplicative_factor", - default = 1.0, - min = -1000.0, - max = 1000.0) - - case object ScribeRankingInfoInSamplingTransform - extends FSParam[Boolean]("sampling_transform_scribe_ranking_info", false) - -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/BUILD b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/BUILD deleted file mode 100644 index 7075167e3..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -scala_library( - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/marshaller", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/marshaller/request", - "stitch/stitch-core", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/CollectionUtil.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/CollectionUtil.scala deleted file mode 100644 index db9a1f9f5..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/CollectionUtil.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.twitter.follow_recommendations.common.utils - -object CollectionUtil { - - /** - * Transposes a sequence of sequences. As opposed to the Scala collection library version - * of transpose, the sequences do not have to have the same length. - * - * Example: - * transpose(immutable.Seq(immutable.Seq(1,2,3), immutable.Seq(4,5), immutable.Seq(6,7))) - * => immutable.Seq(immutable.Seq(1, 4, 6), immutable.Seq(2, 5, 7), immutable.Seq(3)) - * - * @param seq a sequence of sequences - * @tparam A the type of elements in the seq - * @return the transposed sequence of sequences - */ - def transposeLazy[A](seq: Seq[Seq[A]]): Stream[Seq[A]] = - seq.filter(_.nonEmpty) match { - case Nil => Stream.empty - case ys => ys.map(_.head) #:: transposeLazy(ys.map(_.tail)) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/DisplayLocationProductConverterUtil.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/DisplayLocationProductConverterUtil.scala deleted file mode 100644 index 2f6db39b6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/DisplayLocationProductConverterUtil.scala +++ /dev/null @@ -1,27 +0,0 @@ -package com.twitter.follow_recommendations.common.utils - -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.common.models.Product -import com.twitter.product_mixer.core.model.marshalling.request.Product - -object DisplayLocationProductConverterUtil { - def productToDisplayLocation(product: Product): DisplayLocation = { - product match { - case Product.MagicRecs => DisplayLocation.MagicRecs - case _ => - throw UnconvertibleProductMixerProductException( - s"Cannot convert Product Mixer Product ${product.identifier.name} into a FRS DisplayLocation.") - } - } - - def displayLocationToProduct(displayLocation: DisplayLocation): Product = { - displayLocation match { - case DisplayLocation.MagicRecs => Product.MagicRecs - case _ => - throw UnconvertibleProductMixerProductException( - s"Cannot convert DisplayLocation ${displayLocation.toFsName} into a Product Mixer Product.") - } - } -} - -case class UnconvertibleProductMixerProductException(message: String) extends Exception(message) diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/MergeUtil.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/MergeUtil.scala deleted file mode 100644 index 6aaee4c45..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/MergeUtil.scala +++ /dev/null @@ -1,51 +0,0 @@ -package com.twitter.follow_recommendations.common.utils - -object MergeUtil { - - /** - * Takes a seq of items which have weights. Returns an infinite stream of each item - * by their weights. All weights need to be greater than or equal to zero. In addition, - * the sum of weights should be greater than zero. - * - * Example usage of this function: - * Input weighted Item {{CS1, 3}, {CS2, 2}, {CS3, 5}} - * Output stream: (CS1, CS1, CS1, CS2, CS2, CS3, CS3, CS3, CS3, CS3, CS1, CS1, CS1, CS2,...} - * - * @param items items - * @param weighted provides weights for items - * @tparam T type of item - * - * @return Stream of Ts - */ - def weightedRoundRobin[T]( - items: Seq[T] - )( - implicit weighted: Weighted[T] - ): Stream[T] = { - if (items.isEmpty) { - Stream.empty - } else { - val weights = items.map { i => weighted(i) } - assert( - weights.forall { - _ >= 0 - }, - "Negative weight exists for sampling") - val cumulativeWeight = weights.scanLeft(0.0)(_ + _).tail - assert(cumulativeWeight.last > 0, "Sum of the sampling weights is not positive") - - var weightIdx = 0 - var weight = 0 - - def next(): Stream[T] = { - val tmpIdx = weightIdx - weight = weight + 1 - weight = if (weight >= weights(weightIdx)) 0 else weight - weightIdx = if (weight == 0) weightIdx + 1 else weightIdx - weightIdx = if (weightIdx == weights.length) 0 else weightIdx - items(tmpIdx) #:: next() - } - next() - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/RandomUtil.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/RandomUtil.scala deleted file mode 100644 index 9d66e8deb..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/RandomUtil.scala +++ /dev/null @@ -1,88 +0,0 @@ -package com.twitter.follow_recommendations.common.utils -import scala.util.Random - -object RandomUtil { - - /** - * Takes a seq of items which have weights. Returns an infinite stream that is - * sampled with replacement using the weights for each item. All weights need - * to be greater than or equal to zero. In addition, the sum of weights - * should be greater than zero. - * - * @param items items - * @param weighted provides weights for items - * @tparam T type of item - * @return Stream of Ts - */ - def weightedRandomSamplingWithReplacement[T]( - items: Seq[T], - random: Option[Random] = None - )( - implicit weighted: Weighted[T] - ): Stream[T] = { - if (items.isEmpty) { - Stream.empty - } else { - val weights = items.map { i => weighted(i) } - assert(weights.forall { _ >= 0 }, "Negative weight exists for sampling") - val cumulativeWeight = weights.scanLeft(0.0)(_ + _).tail - assert(cumulativeWeight.last > 0, "Sum of the sampling weights is not positive") - val cumulativeProbability = cumulativeWeight map (_ / cumulativeWeight.last) - def next(): Stream[T] = { - val rand = random.getOrElse(Random).nextDouble() - val idx = cumulativeProbability.indexWhere(_ >= rand) - items(if (idx == -1) items.length - 1 else idx) #:: next() - } - next() - } - } - - /** - * Takes a seq of items and their weights. Returns a lazy weighted shuffle of - * the elements in the list. All weights should be greater than zero. - * - * @param items items - * @param weighted provides weights for items - * @tparam T type of item - * @return Stream of Ts - */ - def weightedRandomShuffle[T]( - items: Seq[T], - random: Option[Random] = None - )( - implicit weighted: Weighted[T] - ): Stream[T] = { - assert(items.forall { i => weighted(i) > 0 }, "Non-positive weight exists for shuffling") - def next(it: Seq[T]): Stream[T] = { - if (it.isEmpty) - Stream.empty - else { - val cumulativeWeight = it.scanLeft(0.0)((acc: Double, curr: T) => acc + weighted(curr)).tail - val cutoff = random.getOrElse(Random).nextDouble() * cumulativeWeight.last - val idx = cumulativeWeight.indexWhere(_ >= cutoff) - val (left, right) = it.splitAt(idx) - it(if (idx == -1) it.size - 1 else idx) #:: next(left ++ right.drop(1)) - } - } - next(items) - } - - /** - * Takes a seq of items and a weight function, returns a lazy weighted shuffle of - * the elements in the list.The weight function is based on the rank of the element - * in the original lst. - * @param items - * @param rankToWeight - * @param random - * @tparam T - * @return - */ - def weightedRandomShuffleByRank[T]( - items: Seq[T], - rankToWeight: Int => Double, - random: Option[Random] = None - ): Stream[T] = { - val candWeights = items.zipWithIndex.map { case (item, rank) => (item, rankToWeight(rank)) } - RandomUtil.weightedRandomShuffle(candWeights, random).map(_._1) - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/RescueWithStatsUtils.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/RescueWithStatsUtils.scala deleted file mode 100644 index 8275228d6..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/RescueWithStatsUtils.scala +++ /dev/null @@ -1,50 +0,0 @@ -package com.twitter.follow_recommendations.common.utils - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.stitch.Stitch -import com.twitter.util.Duration -import com.twitter.util.TimeoutException - -object RescueWithStatsUtils { - def rescueWithStats[T]( - s: Stitch[Seq[T]], - stats: StatsReceiver, - source: String - ): Stitch[Seq[T]] = { - StatsUtil.profileStitchSeqResults(s, stats.scope(source)).rescue { - case _: Exception => Stitch.Nil - } - } - - def rescueOptionalWithStats[T]( - s: Stitch[Option[T]], - stats: StatsReceiver, - source: String - ): Stitch[Option[T]] = { - StatsUtil.profileStitchOptionalResults(s, stats.scope(source)).rescue { - case _: Exception => Stitch.None - } - } - - def rescueWithStatsWithin[T]( - s: Stitch[Seq[T]], - stats: StatsReceiver, - source: String, - timeout: Duration - ): Stitch[Seq[T]] = { - val hydratedScopeSource = stats.scope(source) - StatsUtil - .profileStitchSeqResults( - s.within(timeout)(com.twitter.finagle.util.DefaultTimer), - hydratedScopeSource) - .rescue { - case _: TimeoutException => - hydratedScopeSource.counter("timeout").incr() - Stitch.Nil - case _: Exception => - hydratedScopeSource.counter("exception").incr() - Stitch.Nil - } - } -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/UserSignupUtil.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/UserSignupUtil.scala deleted file mode 100644 index 73d90a85b..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/UserSignupUtil.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.twitter.follow_recommendations.common.utils - -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.snowflake.id.SnowflakeId -import com.twitter.util.Duration -import com.twitter.util.Time - -object UserSignupUtil { - def signupTime(hasClientContext: HasClientContext): Option[Time] = - hasClientContext.clientContext.userId.flatMap(SnowflakeId.timeFromIdOpt) - - def userSignupAge(hasClientContext: HasClientContext): Option[Duration] = - signupTime(hasClientContext).map(Time.now - _) -} diff --git a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/Weighted.scala b/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/Weighted.scala deleted file mode 100644 index adb95e5f5..000000000 --- a/follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils/Weighted.scala +++ /dev/null @@ -1,21 +0,0 @@ -package com.twitter.follow_recommendations.common.utils - -/** - * Typeclass for any Recommendation type that has a weight - * - */ -trait Weighted[-Rec] { - def apply(rec: Rec): Double -} - -object Weighted { - implicit object WeightedTuple extends Weighted[(_, Double)] { - override def apply(rec: (_, Double)): Double = rec._2 - } - - def fromFunction[Rec](f: Rec => Double): Weighted[Rec] = { - new Weighted[Rec] { - override def apply(rec: Rec): Double = f(rec) - } - } -} diff --git a/follow-recommendations-service/server/src/main/resources/BUILD b/follow-recommendations-service/server/src/main/resources/BUILD deleted file mode 100644 index 610607ec5..000000000 --- a/follow-recommendations-service/server/src/main/resources/BUILD +++ /dev/null @@ -1,20 +0,0 @@ -resources( - sources = [ - "*.tsv", - "*.xml", - "**/*", - "config/*.yml", - ], -) - -# Created for Bazel compatibility. -# In Bazel, loose files must be part of a target to be included into a bundle. -files( - name = "frs_resources", - sources = [ - "*.tsv", - "*.xml", - "*.yml", - "**/*", - ], -) diff --git a/follow-recommendations-service/server/src/main/resources/config/decider.yml b/follow-recommendations-service/server/src/main/resources/config/decider.yml deleted file mode 100644 index a46626094..000000000 --- a/follow-recommendations-service/server/src/main/resources/config/decider.yml +++ /dev/null @@ -1,129 +0,0 @@ -enable_recommendations: - comment: Proportion of requests where we return an actual response as part. Decreasing the value will increase the portion of empty responses (in order to disable the service) as part of the graceful degradation. - default_availability: 10000 -enable_score_user_candidates: - comment: Proportion of requests where score user candidates from the scoreUserCandidates endpoint - default_availability: 10000 -enable_profile_sidebar_product: - comment: Proportion of requests where we return an actual response for profile sidebar product - default_availability: 10000 -enable_magic_recs_product: - comment: Proportion of requests where we return an actual response for magic recs product - default_availability: 10000 -enable_rux_landing_page_product: - comment: Proportion of requests where we return an actual response for rux landing page product - default_availability: 10000 -enable_rux_pymk_product: - comment: Proportion of requests where we return an actual response for rux pymk product - default_availability: 10000 -enable_profile_bonus_follow_product: - comment: Proportion of requests where we return an actual response for profile bonus follow product - default_availability: 10000 -enable_election_explore_wtf_product: - comment: Proportion of requests where we return an actual response for election explore wtf product - default_availability: 10000 -enable_cluster_follow_product: - comment: Proportion of requests where we return an actual response for cluster follow product - default_availability: 10000 -enable_home_timeline_product: - comment: Proportion of requests where we return an actual response for htl wtf product - default_availability: 10000 -enable_htl_bonus_follow_product: - comment: Proportion of requests where we return an actual response for htl bonus follow product - default_availability: 10000 -enable_explore_tab_product: - comment: Proportion of requests where we return an actual response for explore tab product - default_availability: 10000 -enable_sidebar_product: - comment: Proportion of requests where we return an actual response for sidebar product - default_availability: 10000 -enable_campaign_form_product: - comment: Proportion of requests where we return an actual response for campaign form product - default_availability: 10000 -enable_reactive_follow_product: - comment: Proportion of requests where we return an actual response for reactive follow product - default_availability: 10000 -enable_nux_pymk_product: - comment: Proportion of requests where we return an actual response for nux pymk product - default_availability: 10000 -enable_nux_interests_product: - comment: Proportion of requests where we return an actual response for nux interests product - default_availability: 10000 -enable_nux_topic_bonus_follow_product: - comment: Proportion of requests where we return an actual response for nux topic-based bonus follow product - default_availability: 10000 -enable_india_covid19_curated_accounts_wtf_product: - comment: Proportion of requests where we return an actual response for india covid19 curated accounts wtf product - default_availability: 10000 -enable_ab_upload_product: - comment: Proportion of requests where we return an actual response for the address book upload product - default_availability: 10000 -enable_people_plus_plus_product: - comment: Proportion of requests where we return an actual response for the PeoplePlusPlus/Connect Tab product - default_availability: 10000 -enable_tweet_notification_recs_product: - comment: Proportion of requests where we return an actual response for the Tweet Notification Recommendations product - default_availability: 10000 -enable_profile_device_follow_product: - comment: Proportion of requests where we return an actual response for the ProfileDeviceFollow product - default_availability: 10000 -enable_diffy_module_dark_reading: - comment: Percentage of dark read traffic routed to diffy thrift - default_availability: 0 -enable_recos_backfill_product: - comment: Proportion of requests where we return an actual response for the RecosBackfill product - default_availability: 10000 -enable_post_nux_follow_task_product: - comment: Proportion of requests where we return an actual response for post NUX follow task product - default_availability: 10000 -enable_curated_space_hosts_product: - comment: Proportion of requests where we return an actual response for curated space hosts product - default_availability: 10000 -enable_nux_geo_category_product: - comment: Proportion of requests where we return an actual response for nux geo category product - default_availability: 10000 -enable_nux_interests_category_product: - comment: Proportion of requests where we return an actual response for nux interests category product - default_availability: 10000 -enable_nux_pymk_category_product: - comment: Proportion of requests where we return an actual response for nux pymk category product - default_availability: 10000 -enable_home_timeline_tweet_recs_product: - comment: Proportion of requests where we return an actual response for the Home Timeline Tweet Recs product - default_availability: 10000 -enable_htl_bulk_friend_follows_product: - comment: Proportion of requests where we return an actual response for the HTL bulk friend follows product - default_availability: 10000 -enable_nux_auto_follow_product: - comment: Proportion of requests where we return an actual response for the NUX auto follow product - default_availability: 10000 -enable_search_bonus_follow_product: - comment: Proportion of requests where we return an actual response for search bonus follow product - default_availability: 10000 -enable_fetch_user_in_request_builder: - comment: Proportion of requests where we fetch user object from gizmoduck in request builder - default_availability: 0 -enable_product_mixer_magic_recs_product: - comment: Proportion of requests where we enable the product mixer magic recs product - default_availability: 10000 -enable_home_timeline_reverse_chron_product: - comment: Proportion of requests where we return an actual response for Home timeline reverse chron product - default_availability: 10000 -enable_product_mixer_pipeline_magic_recs_dark_read: - comment: Compare product mixer pipeline responses to current FRS pipeline responses for Magic Recs - default_availability: 0 -enable_experimental_caching: - comment: Proportion of requests we use experimental caching for data caching - default_availability: 0 -enable_distributed_caching: - comment: Proportion of requests we use a distributed cache cluster for data caching - default_availability: 10000 -enable_gizmoduck_caching: - comment: Proportion of requests we use a distributed cache cluster for data caching in Gizmoduck - default_availability: 10000 -enable_traffic_dark_reading: - comment: Proportion of requests where we replicate the request for traffic dark reading - default_availability: 0 -enable_graph_feature_service_requests: - comment: Proportion of requests where we allow request calls to Graph Feature Service - default_availability: 10000 diff --git a/follow-recommendations-service/server/src/main/resources/logback.xml b/follow-recommendations-service/server/src/main/resources/logback.xml deleted file mode 100644 index 96348c8e4..000000000 --- a/follow-recommendations-service/server/src/main/resources/logback.xml +++ /dev/null @@ -1,133 +0,0 @@ - - - - - - - - - - - - - - - - - true - - - - - ${log.service.output} - - ${log.service.output}.%i - 1 - 5 - - - 50MB - - - %date %.-3level ${DEFAULT_SERVICE_PATTERN}%n - - - - - - ${log.access.output} - - ${log.access.output}.%i - 1 - 5 - - - 50MB - - - ${DEFAULT_ACCESS_PATTERN}%n - - - - - - true - ${log.lens.category} - ${log.lens.index} - ${log.lens.tag}/service - - %msg - - - - - - true - ${log.lens.category} - ${log.lens.index} - ${log.lens.tag}/access - - %msg - - - - - - - - - - - - ${async_queue_size} - ${async_max_flush_time} - - - - - ${async_queue_size} - ${async_max_flush_time} - - - - - ${async_queue_size} - ${async_max_flush_time} - - - - - ${async_queue_size} - ${async_max_flush_time} - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/follow-recommendations-service/server/src/main/resources/quality/stp_models/20141223/epModel b/follow-recommendations-service/server/src/main/resources/quality/stp_models/20141223/epModel deleted file mode 100644 index a08d9723c..000000000 --- a/follow-recommendations-service/server/src/main/resources/quality/stp_models/20141223/epModel +++ /dev/null @@ -1,8 +0,0 @@ -# OWNER = jdeng -# Date = 20141223_153423 -# Training Size = 16744473 -# Testing Size = 16767335 -# trained with ElasticNetCV alpha=0.05 cv_folds=5 best_lambda=1.0E-7 -# num base features: 10 -# num nonzero weights: 30 -{bias:-5.67151,featureMetadataMap:["fwd_email":{metadata:{featureWeight:{weight:2.47389}}},"rev_phone":{metadata:{featureWeight:{weight:1.88433}}},"mutual_follow_path":{metadata:{featureWeight:{intervalWeights:[{left:47.0,weight:6.31809},{left:11.0,right:16.0,weight:4.52959},{left:31.0,right:47.0,weight:5.7101},{right:2.0,weight:0.383515},{left:24.0,right:31.0,weight:5.26515},{left:3.0,right:4.0,weight:2.91751},{left:2.0,right:3.0,weight:2.22851},{left:4.0,right:5.0,weight:3.28515},{left:8.0,right:11.0,weight:4.14731},{left:5.0,right:8.0,weight:3.73588},{left:16.0,right:24.0,weight:4.90908}]}}},"fwd_phone":{metadata:{featureWeight:{weight:2.07327}}},"fwd_email_path":{metadata:{featureWeight:{weight:0.961773}}},"rev_phone_path":{metadata:{featureWeight:{weight:0.354484}}},"low_tweepcred_follow_path":{metadata:{featureWeight:{intervalWeights:[{left:4.0,right:5.0,weight:0.177209},{left:7.0,right:8.0,weight:0.12378},{left:3.0,right:4.0,weight:0.197566},{left:5.0,right:6.0,weight:0.15867},{left:2.0,right:3.0,weight:0.196539},{right:2.0,weight:0.1805},{left:75.0,weight:-0.424598},{left:6.0,right:7.0,weight:0.143698},{left:10.0,right:13.0,weight:0.0458502},{left:8.0,right:10.0,weight:0.0919314},{left:13.0,right:75.0,weight:-0.111484}]}}},"rev_email_path":{metadata:{featureWeight:{weight:0.654451}}},"rev_email":{metadata:{featureWeight:{weight:2.33859}}},"fwd_phone_path":{metadata:{featureWeight:{weight:0.210418}}}]} diff --git a/follow-recommendations-service/server/src/main/resources/quality/stp_models/20141223/trainingConfig b/follow-recommendations-service/server/src/main/resources/quality/stp_models/20141223/trainingConfig deleted file mode 100644 index ab38990dd..000000000 --- a/follow-recommendations-service/server/src/main/resources/quality/stp_models/20141223/trainingConfig +++ /dev/null @@ -1 +0,0 @@ -{input:{context:"discover.prod",startDateTime:"",endDateTime:"",trainingFeatures:["STP_FEATURES":["fwd_email","mutual_follow_path","fwd_email_path","rev_phone_path","low_tweepcred_follow_path","rev_phone","fwd_phone","rev_email_path","rev_email","fwd_phone_path"]],engagementActions:["click","favorite","open_link","open","send_tweet","send_reply","retweet","reply","profile_click","follow"],impressionActions:["discard","results","impression"],dataFormat:1,dataPath:"",isLabeled:0},sample:{positiveSampleRatio:1.0,negativeSampleRatio:1.0,sampleType:1},split:{trainingDataSplitSize:0.5,testingDataSplitSize:0.5,splitType:2},transform:{},filter:{featureOptions:[]},join:{engagementRules:["discover"],contentIdType:"tweet",groupBucketSize:3600000},discretize:{}} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/BUILD deleted file mode 100644 index 4dcbaab44..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/BUILD +++ /dev/null @@ -1,48 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/org/slf4j:slf4j-api", - "finagle/finagle-http/src/main/scala", - "finagle/finagle-thriftmux/src/main/scala", - "finatra-internal/decider/src/main/scala", - "finatra-internal/international/src/main/scala/com/twitter/finatra/international/modules", - "finatra-internal/mtls-http/src/main/scala", - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/http-core/src/main/java/com/twitter/finatra/http", - "finatra/inject/inject-app/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "finatra/inject/inject-server/src/main/scala", - "finatra/inject/inject-thrift-client", - "finatra/jackson/src/main/scala/com/twitter/finatra/jackson/modules", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/addressbook", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/adserver", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/cache", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/email_storage_service", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/gizmoduck", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/graph_feature_service", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/phone_storage_service", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/socialgraph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/strato", - "follow-recommendations-service/server/src/main/resources", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/exceptions", - "follow-recommendations-service/thrift/src/main/thrift:thrift-scala", - "geoduck/service/src/main/scala/com/twitter/geoduck/service/common/clientmodules", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/controllers", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/functional_component/configapi", - "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/scala/com/twitter/product_mixer/core/product/guice", - "twitter-server/server/src/main/scala", - "util/util-app/src/main/scala", - "util/util-core:scala", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/FollowRecommendationsServiceThriftServer.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/FollowRecommendationsServiceThriftServer.scala deleted file mode 100644 index fba889c2f..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/FollowRecommendationsServiceThriftServer.scala +++ /dev/null @@ -1,118 +0,0 @@ -package com.twitter.follow_recommendations - -import com.google.inject.Module -import com.twitter.finagle.ThriftMux -import com.twitter.finatra.decider.modules.DeciderModule -import com.twitter.finatra.http.HttpServer -import com.twitter.finatra.http.routing.HttpRouter -import com.twitter.finatra.international.modules.I18nFactoryModule -import com.twitter.finatra.international.modules.LanguagesModule -import com.twitter.finatra.jackson.modules.ScalaObjectMapperModule -import com.twitter.finatra.mtls.http.{Mtls => HttpMtls} -import com.twitter.finatra.mtls.thriftmux.Mtls -import com.twitter.finatra.thrift.ThriftServer -import com.twitter.finatra.thrift.filters._ -import com.twitter.finagle.thrift.Protocols -import com.twitter.finatra.thrift.routing.ThriftRouter -import com.twitter.follow_recommendations.common.clients.addressbook.AddressbookModule -import com.twitter.follow_recommendations.common.clients.adserver.AdserverModule -import com.twitter.follow_recommendations.common.clients.cache.MemcacheModule -import com.twitter.follow_recommendations.common.clients.deepbirdv2.DeepBirdV2PredictionServiceClientModule -import com.twitter.follow_recommendations.common.clients.email_storage_service.EmailStorageServiceModule -import com.twitter.follow_recommendations.common.clients.geoduck.LocationServiceModule -import com.twitter.follow_recommendations.common.clients.gizmoduck.GizmoduckModule -import com.twitter.follow_recommendations.common.clients.graph_feature_service.GraphFeatureStoreModule -import com.twitter.follow_recommendations.common.clients.impression_store.ImpressionStoreModule -import com.twitter.follow_recommendations.common.clients.phone_storage_service.PhoneStorageServiceModule -import com.twitter.follow_recommendations.common.clients.socialgraph.SocialGraphModule -import com.twitter.follow_recommendations.common.clients.strato.StratoClientModule -import com.twitter.follow_recommendations.common.constants.ServiceConstants._ -import com.twitter.follow_recommendations.common.feature_hydration.sources.HydrationSourcesModule -import com.twitter.follow_recommendations.controllers.ThriftController -import com.twitter.follow_recommendations.modules._ -import com.twitter.follow_recommendations.service.exceptions.UnknownLoggingExceptionMapper -import com.twitter.follow_recommendations.services.FollowRecommendationsServiceWarmupHandler -import com.twitter.follow_recommendations.thriftscala.FollowRecommendationsThriftService -import com.twitter.geoduck.service.common.clientmodules.ReverseGeocoderThriftClientModule -import com.twitter.inject.thrift.filters.DarkTrafficFilter -import com.twitter.inject.thrift.modules.ThriftClientIdModule -import com.twitter.product_mixer.core.controllers.ProductMixerController -import com.twitter.product_mixer.core.module.PipelineExecutionLoggerModule -import com.twitter.product_mixer.core.module.product_mixer_flags.ProductMixerFlagModule -import com.twitter.product_mixer.core.module.stringcenter.ProductScopeStringCenterModule -import com.twitter.product_mixer.core.product.guice.ProductScopeModule - -object FollowRecommendationsServiceThriftServerMain extends FollowRecommendationsServiceThriftServer - -class FollowRecommendationsServiceThriftServer - extends ThriftServer - with Mtls - with HttpServer - with HttpMtls { - override val name: String = "follow-recommendations-service-server" - - override val modules: Seq[Module] = - Seq( - ABDeciderModule, - AddressbookModule, - AdserverModule, - ConfigApiModule, - DeciderModule, - DeepBirdV2PredictionServiceClientModule, - DiffyModule, - EmailStorageServiceModule, - FeaturesSwitchesModule, - FlagsModule, - GizmoduckModule, - GraphFeatureStoreModule, - HydrationSourcesModule, - I18nFactoryModule, - ImpressionStoreModule, - LanguagesModule, - LocationServiceModule, - MemcacheModule, - PhoneStorageServiceModule, - PipelineExecutionLoggerModule, - ProductMixerFlagModule, - ProductRegistryModule, - new ProductScopeModule(), - new ProductScopeStringCenterModule(), - new ReverseGeocoderThriftClientModule, - ScalaObjectMapperModule, - ScorerModule, - ScribeModule, - SocialGraphModule, - StratoClientModule, - ThriftClientIdModule, - TimerModule, - ) - - def configureThrift(router: ThriftRouter): Unit = { - router - .filter[LoggingMDCFilter] - .filter[TraceIdMDCFilter] - .filter[ThriftMDCFilter] - .filter[StatsFilter] - .filter[AccessLoggingFilter] - .filter[ExceptionMappingFilter] - .exceptionMapper[UnknownLoggingExceptionMapper] - .filter[DarkTrafficFilter[FollowRecommendationsThriftService.ReqRepServicePerEndpoint]] - .add[ThriftController] - } - - override def configureThriftServer(server: ThriftMux.Server): ThriftMux.Server = { - server.withProtocolFactory( - Protocols.binaryFactory( - stringLengthLimit = StringLengthLimit, - containerLengthLimit = ContainerLengthLimit)) - } - - override def configureHttp(router: HttpRouter): Unit = router.add( - ProductMixerController[FollowRecommendationsThriftService.MethodPerEndpoint]( - this.injector, - FollowRecommendationsThriftService.ExecutePipeline)) - - override def warmup(): Unit = { - handle[FollowRecommendationsServiceWarmupHandler]() - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Action.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Action.scala deleted file mode 100644 index cd3d88967..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Action.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.assembler.models - -import com.twitter.follow_recommendations.{thriftscala => t} - -case class Action(text: String, actionURL: String) { - lazy val toThrift: t.Action = { - t.Action(text, actionURL) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/BUILD deleted file mode 100644 index 65d265480..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/BUILD +++ /dev/null @@ -1,12 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/thrift/src/main/thrift:thrift-scala", - "stringcenter/client", - ], - exports = [ - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Config.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Config.scala deleted file mode 100644 index 3346a05e1..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Config.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.twitter.follow_recommendations.assembler.models - -import com.twitter.stringcenter.client.core.ExternalString - -case class HeaderConfig(title: TitleConfig) -case class TitleConfig(text: ExternalString) -case class FooterConfig(actionConfig: Option[ActionConfig]) -case class ActionConfig(footerText: ExternalString, actionURL: String) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/FeedbackAction.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/FeedbackAction.scala deleted file mode 100644 index 25caa933a..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/FeedbackAction.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.twitter.follow_recommendations.assembler.models - -import com.twitter.follow_recommendations.{thriftscala => t} - -trait FeedbackAction { - def toThrift: t.FeedbackAction -} - -case class DismissUserId() extends FeedbackAction { - override lazy val toThrift: t.FeedbackAction = { - t.FeedbackAction.DismissUserId(t.DismissUserId()) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Footer.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Footer.scala deleted file mode 100644 index f62368431..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Footer.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.assembler.models - -import com.twitter.follow_recommendations.{thriftscala => t} - -case class Footer(action: Option[Action]) { - lazy val toThrift: t.Footer = { - t.Footer(action.map(_.toThrift)) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Header.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Header.scala deleted file mode 100644 index 58c60c789..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Header.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.assembler.models - -import com.twitter.follow_recommendations.{thriftscala => t} - -case class Header(title: Title) { - lazy val toThrift: t.Header = { - t.Header(title.toThrift) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Layout.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Layout.scala deleted file mode 100644 index f0dc9630a..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Layout.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.twitter.follow_recommendations.assembler.models - -sealed trait Layout - -case class UserListLayout( - header: Option[HeaderConfig], - userListOptions: UserListOptions, - socialProofs: Option[Seq[SocialProof]], - footer: Option[FooterConfig]) - extends Layout - -case class CarouselLayout( - header: Option[HeaderConfig], - carouselOptions: CarouselOptions, - socialProofs: Option[Seq[SocialProof]]) - extends Layout diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/RecommendationOptions.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/RecommendationOptions.scala deleted file mode 100644 index 72351e033..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/RecommendationOptions.scala +++ /dev/null @@ -1,11 +0,0 @@ -package com.twitter.follow_recommendations.assembler.models - -sealed trait RecommendationOptions - -case class UserListOptions( - userBioEnabled: Boolean, - userBioTruncated: Boolean, - userBioMaxLines: Option[Long], -) extends RecommendationOptions - -case class CarouselOptions() extends RecommendationOptions diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/SocialProof.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/SocialProof.scala deleted file mode 100644 index fd5878af8..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/SocialProof.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.twitter.follow_recommendations.assembler.models - -import com.twitter.stringcenter.client.core.ExternalString - -sealed trait SocialProof - -case class GeoContextProof(popularInCountryText: ExternalString) extends SocialProof -case class FollowedByUsersProof(text1: ExternalString, text2: ExternalString, textN: ExternalString) - extends SocialProof - -sealed trait SocialText { - def text: String -} - -case class GeoSocialText(text: String) extends SocialText -case class FollowedByUsersText(text: String) extends SocialText diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Title.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Title.scala deleted file mode 100644 index 3d128e7c8..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/Title.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.assembler.models - -import com.twitter.follow_recommendations.{thriftscala => t} - -case class Title(text: String) { - lazy val toThrift: t.Title = { - t.Title(text) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/WTFPresentation.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/WTFPresentation.scala deleted file mode 100644 index 7cfda1846..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models/WTFPresentation.scala +++ /dev/null @@ -1,47 +0,0 @@ -package com.twitter.follow_recommendations.assembler.models - -import com.twitter.follow_recommendations.{thriftscala => t} - -trait WTFPresentation { - def toThrift: t.WTFPresentation -} - -case class UserList( - userBioEnabled: Boolean, - userBioTruncated: Boolean, - userBioMaxLines: Option[Long], - feedbackAction: Option[FeedbackAction]) - extends WTFPresentation { - def toThrift: t.WTFPresentation = { - t.WTFPresentation.UserBioList( - t.UserList(userBioEnabled, userBioTruncated, userBioMaxLines, feedbackAction.map(_.toThrift))) - } -} - -object UserList { - def fromUserListOptions( - userListOptions: UserListOptions - ): UserList = { - UserList( - userListOptions.userBioEnabled, - userListOptions.userBioTruncated, - userListOptions.userBioMaxLines, - None) - } -} - -case class Carousel( - feedbackAction: Option[FeedbackAction]) - extends WTFPresentation { - def toThrift: t.WTFPresentation = { - t.WTFPresentation.Carousel(t.Carousel(feedbackAction.map(_.toThrift))) - } -} - -object Carousel { - def fromCarouselOptions( - carouselOptions: CarouselOptions - ): Carousel = { - Carousel(None) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders/BUILD deleted file mode 100644 index 81d912a99..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders/BUILD +++ /dev/null @@ -1,16 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders/PromotedAccountsBlender.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders/PromotedAccountsBlender.scala deleted file mode 100644 index 8516de53d..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders/PromotedAccountsBlender.scala +++ /dev/null @@ -1,138 +0,0 @@ -package com.twitter.follow_recommendations.blenders - -import com.google.common.annotations.VisibleForTesting -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.follow_recommendations.common.models.AdMetadata -import com.twitter.follow_recommendations.common.models.Recommendation -import com.twitter.inject.Logging -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PromotedAccountsBlender @Inject() (statsReceiver: StatsReceiver) - extends Transform[Int, Recommendation] - with Logging { - - import PromotedAccountsBlender._ - val stats = statsReceiver.scope(Name) - val inputOrganicAccounts = stats.counter(InputOrganic) - val inputPromotedAccounts = stats.counter(InputPromoted) - val outputOrganicAccounts = stats.counter(OutputOrganic) - val outputPromotedAccounts = stats.counter(OutputPromoted) - val promotedAccountsStats = stats.scope(NumPromotedAccounts) - - override def transform( - maxResults: Int, - items: Seq[Recommendation] - ): Stitch[Seq[Recommendation]] = { - val (promoted, organic) = items.partition(_.isPromotedAccount) - val promotedIds = promoted.map(_.id).toSet - val dedupedOrganic = organic.filterNot(u => promotedIds.contains(u.id)) - val blended = blendPromotedAccount(dedupedOrganic, promoted, maxResults) - val (outputPromoted, outputOrganic) = blended.partition(_.isPromotedAccount) - inputOrganicAccounts.incr(dedupedOrganic.size) - inputPromotedAccounts.incr(promoted.size) - outputOrganicAccounts.incr(outputOrganic.size) - val size = outputPromoted.size - outputPromotedAccounts.incr(size) - if (size <= 5) { - promotedAccountsStats.counter(outputPromoted.size.toString).incr() - } else { - promotedAccountsStats.counter(MoreThan5Promoted).incr() - } - Stitch.value(blended) - } - - /** - * Merge Promoted results and organic results. Promoted result dictates the position - * in the merge list. - * - * merge a list of positioned users, aka. promoted, and a list of organic - * users. The positioned promoted users are pre-sorted with regards to their - * position ascendingly. Only requirement about position is to be within the - * range, i.e, can not exceed the combined length if merge is successful, ok - * to be at the last position, but not beyond. - * For more detailed description of location position: - * http://confluence.local.twitter.com/display/ADS/Promoted+Tweets+in+Timeline+Design+Document - */ - @VisibleForTesting - private[blenders] def mergePromotedAccounts( - organicUsers: Seq[Recommendation], - promotedUsers: Seq[Recommendation] - ): Seq[Recommendation] = { - def mergeAccountWithIndex( - organicUsers: Seq[Recommendation], - promotedUsers: Seq[Recommendation], - index: Int - ): Stream[Recommendation] = { - if (promotedUsers.isEmpty) organicUsers.toStream - else { - val promotedHead = promotedUsers.head - val promotedTail = promotedUsers.tail - promotedHead.adMetadata match { - case Some(AdMetadata(position, _)) => - if (position < 0) mergeAccountWithIndex(organicUsers, promotedTail, index) - else if (position == index) - promotedHead #:: mergeAccountWithIndex(organicUsers, promotedTail, index) - else if (organicUsers.isEmpty) organicUsers.toStream - else { - val organicHead = organicUsers.head - val organicTail = organicUsers.tail - organicHead #:: mergeAccountWithIndex(organicTail, promotedUsers, index + 1) - } - case _ => - logger.error("Unknown Candidate type in mergePromotedAccounts") - Stream.empty - } - } - } - - mergeAccountWithIndex(organicUsers, promotedUsers, 0) - } - - private[this] def blendPromotedAccount( - organic: Seq[Recommendation], - promoted: Seq[Recommendation], - maxResults: Int - ): Seq[Recommendation] = { - - val merged = mergePromotedAccounts(organic, promoted) - val mergedServed = merged.take(maxResults) - val promotedServed = promoted.intersect(mergedServed) - - if (isBlendPromotedNeeded( - mergedServed.size - promotedServed.size, - promotedServed.size, - maxResults - )) { - mergedServed - } else { - organic.take(maxResults) - } - } - - @VisibleForTesting - private[blenders] def isBlendPromotedNeeded( - organicSize: Int, - promotedSize: Int, - maxResults: Int - ): Boolean = { - (organicSize > 1) && - (promotedSize > 0) && - (promotedSize < organicSize) && - (promotedSize <= 2) && - (maxResults > 1) - } -} - -object PromotedAccountsBlender { - val Name = "promoted_accounts_blender" - val InputOrganic = "input_organic_accounts" - val InputPromoted = "input_promoted_accounts" - val OutputOrganic = "output_organic_accounts" - val OutputPromoted = "output_promoted_accounts" - val NumPromotedAccounts = "num_promoted_accounts" - val MoreThan5Promoted = "more_than_5" -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/BUILD deleted file mode 100644 index f64fcda69..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/BUILD +++ /dev/null @@ -1,28 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "configapi/configapi-core", - "configapi/configapi-decider", - "configapi/configapi-featureswitches:v2", - "featureswitches/featureswitches-core", - "featureswitches/featureswitches-core:v2", - "featureswitches/featureswitches-core/src/main/scala/com/twitter/featureswitches/v2/builder", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/top_organic_follows_accounts", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/ConfigBuilder.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/ConfigBuilder.scala deleted file mode 100644 index 818f4402c..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/ConfigBuilder.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.twitter.follow_recommendations.configapi - -import com.twitter.timelines.configapi.CompositeConfig -import com.twitter.timelines.configapi.Config -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ConfigBuilder @Inject() ( - deciderConfigs: DeciderConfigs, - featureSwitchConfigs: FeatureSwitchConfigs) { - // The order of configs added to `CompositeConfig` is important. The config will be matched with - // the first possible rule. So, current setup will give priority to Deciders instead of FS - def build(): Config = - new CompositeConfig(Seq(deciderConfigs.config, featureSwitchConfigs.config)) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/DeciderConfigs.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/DeciderConfigs.scala deleted file mode 100644 index 0154a1703..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/DeciderConfigs.scala +++ /dev/null @@ -1,52 +0,0 @@ -package com.twitter.follow_recommendations.configapi - -import com.twitter.decider.Recipient -import com.twitter.decider.SimpleRecipient -import com.twitter.follow_recommendations.configapi.deciders.DeciderKey -import com.twitter.follow_recommendations.configapi.deciders.DeciderParams -import com.twitter.follow_recommendations.products.home_timeline_tweet_recs.configapi.HomeTimelineTweetRecsParams -import com.twitter.servo.decider.DeciderGateBuilder -import com.twitter.timelines.configapi._ -import com.twitter.timelines.configapi.decider.DeciderSwitchOverrideValue -import com.twitter.timelines.configapi.decider.GuestRecipient -import com.twitter.timelines.configapi.decider.RecipientBuilder -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class DeciderConfigs @Inject() (deciderGateBuilder: DeciderGateBuilder) { - val overrides: Seq[OptionalOverride[_]] = DeciderConfigs.ParamsToDeciderMap.map { - case (params, deciderKey) => - params.optionalOverrideValue( - DeciderSwitchOverrideValue( - feature = deciderGateBuilder.keyToFeature(deciderKey), - enabledValue = true, - recipientBuilder = DeciderConfigs.UserOrGuestOrRequest - ) - ) - }.toSeq - - val config: BaseConfig = BaseConfigBuilder(overrides).build("FollowRecommendationServiceDeciders") -} - -object DeciderConfigs { - val ParamsToDeciderMap = Map( - DeciderParams.EnableRecommendations -> DeciderKey.EnableRecommendations, - DeciderParams.EnableScoreUserCandidates -> DeciderKey.EnableScoreUserCandidates, - HomeTimelineTweetRecsParams.EnableProduct -> DeciderKey.EnableHomeTimelineTweetRecsProduct, - ) - - object UserOrGuestOrRequest extends RecipientBuilder { - - def apply(requestContext: BaseRequestContext): Option[Recipient] = requestContext match { - case c: WithUserId if c.userId.isDefined => - c.userId.map(SimpleRecipient) - case c: WithGuestId if c.guestId.isDefined => - c.guestId.map(GuestRecipient) - case c: WithGuestId => - RecipientBuilder.Request(c) - case _ => - throw new UndefinedUserIdNorGuestIDException(requestContext) - } - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/FeatureSwitchConfigs.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/FeatureSwitchConfigs.scala deleted file mode 100644 index c7f7f6d9e..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/FeatureSwitchConfigs.scala +++ /dev/null @@ -1,138 +0,0 @@ -package com.twitter.follow_recommendations.configapi - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.candidate_sources.base.SocialProofEnforcedCandidateSourceFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts.CrowdSearchAccountsFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeoQualityFollowSourceFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.top_organic_follows_accounts.TopOrganicFollowsAccountsFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeoSourceFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow.PPMILocaleFollowSourceFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.real_graph.RealGraphOonFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.recent_engagement.RepeatedProfileVisitsFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.sims.SimsSourceFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentEngagementSimilarUsersFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.SimsExpansionFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.socialgraph.RecentFollowingRecentFollowingExpansionSourceFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.stp.OfflineStpSourceFsConfig -import com.twitter.follow_recommendations.common.candidate_sources.stp.OnlineSTPSourceFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.triangular_loops.TriangularLoopsFSConfig -import com.twitter.follow_recommendations.common.candidate_sources.user_user_graph.UserUserGraphFSConfig -import com.twitter.follow_recommendations.common.feature_hydration.sources.FeatureHydrationSourcesFSConfig -import com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker.WeightedCandidateSourceRankerFSConfig -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.follow_recommendations.flows.content_recommender_flow.ContentRecommenderFlowFSConfig -import com.twitter.follow_recommendations.common.predicates.gizmoduck.GizmoduckPredicateFSConfig -import com.twitter.follow_recommendations.common.predicates.hss.HssPredicateFSConfig -import com.twitter.follow_recommendations.common.predicates.sgs.SgsPredicateFSConfig -import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlFlowFSConfig -import com.twitter.logging.Logger -import com.twitter.timelines.configapi.BaseConfigBuilder -import com.twitter.timelines.configapi.FeatureSwitchOverrideUtil - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class FeatureSwitchConfigs @Inject() ( - globalFeatureSwitchConfig: GlobalFeatureSwitchConfig, - featureHydrationSourcesFSConfig: FeatureHydrationSourcesFSConfig, - weightedCandidateSourceRankerFSConfig: WeightedCandidateSourceRankerFSConfig, - // Flow related config - contentRecommenderFlowFSConfig: ContentRecommenderFlowFSConfig, - postNuxMlFlowFSConfig: PostNuxMlFlowFSConfig, - // Candidate source related config - crowdSearchAccountsFSConfig: CrowdSearchAccountsFSConfig, - offlineStpSourceFsConfig: OfflineStpSourceFsConfig, - onlineSTPSourceFSConfig: OnlineSTPSourceFSConfig, - popGeoSourceFSConfig: PopGeoSourceFSConfig, - popGeoQualityFollowFSConfig: PopGeoQualityFollowSourceFSConfig, - realGraphOonFSConfig: RealGraphOonFSConfig, - repeatedProfileVisitsFSConfig: RepeatedProfileVisitsFSConfig, - recentEngagementSimilarUsersFSConfig: RecentEngagementSimilarUsersFSConfig, - recentFollowingRecentFollowingExpansionSourceFSConfig: RecentFollowingRecentFollowingExpansionSourceFSConfig, - simsExpansionFSConfig: SimsExpansionFSConfig, - simsSourceFSConfig: SimsSourceFSConfig, - socialProofEnforcedCandidateSourceFSConfig: SocialProofEnforcedCandidateSourceFSConfig, - triangularLoopsFSConfig: TriangularLoopsFSConfig, - userUserGraphFSConfig: UserUserGraphFSConfig, - // Predicate related configs - gizmoduckPredicateFSConfig: GizmoduckPredicateFSConfig, - hssPredicateFSConfig: HssPredicateFSConfig, - sgsPredicateFSConfig: SgsPredicateFSConfig, - ppmiLocaleSourceFSConfig: PPMILocaleFollowSourceFSConfig, - topOrganicFollowsAccountsFSConfig: TopOrganicFollowsAccountsFSConfig, - statsReceiver: StatsReceiver) { - - val logger = Logger(classOf[FeatureSwitchConfigs]) - - val mergedFSConfig = - FeatureSwitchConfig.merge( - Seq( - globalFeatureSwitchConfig, - featureHydrationSourcesFSConfig, - weightedCandidateSourceRankerFSConfig, - // Flow related config - contentRecommenderFlowFSConfig, - postNuxMlFlowFSConfig, - // Candidate source related config - crowdSearchAccountsFSConfig, - offlineStpSourceFsConfig, - onlineSTPSourceFSConfig, - popGeoSourceFSConfig, - popGeoQualityFollowFSConfig, - realGraphOonFSConfig, - repeatedProfileVisitsFSConfig, - recentEngagementSimilarUsersFSConfig, - recentFollowingRecentFollowingExpansionSourceFSConfig, - simsExpansionFSConfig, - simsSourceFSConfig, - socialProofEnforcedCandidateSourceFSConfig, - triangularLoopsFSConfig, - userUserGraphFSConfig, - // Predicate related configs: - gizmoduckPredicateFSConfig, - hssPredicateFSConfig, - sgsPredicateFSConfig, - ppmiLocaleSourceFSConfig, - topOrganicFollowsAccountsFSConfig, - ) - ) - - /** - * enum params have to be listed in this main file together as otherwise we'll have to pass in - * some signature like `Seq[FSEnumParams[_]]` which are generics of generics and won't compile. - * we only have enumFsParams from globalFeatureSwitchConfig at the moment - */ - val enumOverrides = globalFeatureSwitchConfig.enumFsParams.flatMap { enumParam => - FeatureSwitchOverrideUtil.getEnumFSOverrides(statsReceiver, logger, enumParam) - } - - val gatedOverrides = mergedFSConfig.gatedOverridesMap.flatMap { - case (fsName, overrides) => - FeatureSwitchOverrideUtil.gatedOverrides(fsName, overrides: _*) - } - - val enumSeqOverrides = globalFeatureSwitchConfig.enumSeqFsParams.flatMap { enumSeqParam => - FeatureSwitchOverrideUtil.getEnumSeqFSOverrides(statsReceiver, logger, enumSeqParam) - } - - val overrides = - FeatureSwitchOverrideUtil - .getBooleanFSOverrides(mergedFSConfig.booleanFSParams: _*) ++ - FeatureSwitchOverrideUtil - .getBoundedIntFSOverrides(mergedFSConfig.intFSParams: _*) ++ - FeatureSwitchOverrideUtil - .getBoundedLongFSOverrides(mergedFSConfig.longFSParams: _*) ++ - FeatureSwitchOverrideUtil - .getBoundedDoubleFSOverrides(mergedFSConfig.doubleFSParams: _*) ++ - FeatureSwitchOverrideUtil - .getDurationFSOverrides(mergedFSConfig.durationFSParams: _*) ++ - FeatureSwitchOverrideUtil - .getBoundedOptionalDoubleOverrides(mergedFSConfig.optionalDoubleFSParams: _*) ++ - FeatureSwitchOverrideUtil.getStringSeqFSOverrides(mergedFSConfig.stringSeqFSParams: _*) ++ - enumOverrides ++ - gatedOverrides ++ - enumSeqOverrides - - val config = BaseConfigBuilder(overrides).build("FollowRecommendationServiceFeatureSwitches") -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/GlobalFeatureSwitchConfig.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/GlobalFeatureSwitchConfig.scala deleted file mode 100644 index 537ed660c..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/GlobalFeatureSwitchConfig.scala +++ /dev/null @@ -1,49 +0,0 @@ -package com.twitter.follow_recommendations.configapi - -import com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts.CrowdSearchAccountsParams.AccountsFilteringAndRankingLogics -import com.twitter.follow_recommendations.common.candidate_sources.top_organic_follows_accounts.TopOrganicFollowsAccountsParams.{ - AccountsFilteringAndRankingLogics => OrganicAccountsFilteringAndRankingLogics -} -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentEngagementSimilarUsersParams -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.SimsExpansionSourceParams -import com.twitter.follow_recommendations.common.rankers.ml_ranker.ranking.MlRankerParams.CandidateScorerIdParam -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.follow_recommendations.configapi.params.GlobalParams.CandidateSourcesToFilter -import com.twitter.follow_recommendations.configapi.params.GlobalParams.EnableCandidateParamHydrations -import com.twitter.follow_recommendations.configapi.params.GlobalParams.EnableGFSSocialProofTransform -import com.twitter.follow_recommendations.configapi.params.GlobalParams.EnableRecommendationFlowLogs -import com.twitter.follow_recommendations.configapi.params.GlobalParams.EnableWhoToFollowProducts -import com.twitter.follow_recommendations.configapi.params.GlobalParams.KeepSocialUserCandidate -import com.twitter.follow_recommendations.configapi.params.GlobalParams.KeepUserCandidate -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.Param -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class GlobalFeatureSwitchConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = { - Seq( - EnableCandidateParamHydrations, - KeepUserCandidate, - KeepSocialUserCandidate, - EnableGFSSocialProofTransform, - EnableWhoToFollowProducts, - EnableRecommendationFlowLogs - ) - } - - val enumFsParams = - Seq( - CandidateScorerIdParam, - SimsExpansionSourceParams.Aggregator, - RecentEngagementSimilarUsersParams.Aggregator, - CandidateSourcesToFilter, - ) - - val enumSeqFsParams = - Seq( - AccountsFilteringAndRankingLogics, - OrganicAccountsFilteringAndRankingLogics - ) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/ParamsFactory.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/ParamsFactory.scala deleted file mode 100644 index 847d69962..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/ParamsFactory.scala +++ /dev/null @@ -1,29 +0,0 @@ -package com.twitter.follow_recommendations.configapi - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext -import com.twitter.servo.util.MemoizingStatsReceiver -import com.twitter.timelines.configapi.Config -import com.twitter.timelines.configapi.FeatureValue -import com.twitter.timelines.configapi.Params -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ParamsFactory @Inject() ( - config: Config, - requestContextFactory: RequestContextFactory, - statsReceiver: StatsReceiver) { - - private val stats = new MemoizingStatsReceiver(statsReceiver.scope("configapi")) - def apply(followRecommendationServiceRequestContext: RequestContext): Params = - config(followRecommendationServiceRequestContext, stats) - - def apply( - clientContext: ClientContext, - displayLocation: DisplayLocation, - featureOverrides: Map[String, FeatureValue] - ): Params = - apply(requestContextFactory(clientContext, displayLocation, featureOverrides)) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/RequestContext.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/RequestContext.scala deleted file mode 100644 index ebc8abf3c..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/RequestContext.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.twitter.follow_recommendations.configapi - -import com.twitter.timelines.configapi.BaseRequestContext -import com.twitter.timelines.configapi.FeatureContext -import com.twitter.timelines.configapi.NullFeatureContext -import com.twitter.timelines.configapi.GuestId -import com.twitter.timelines.configapi.UserId -import com.twitter.timelines.configapi.WithFeatureContext -import com.twitter.timelines.configapi.WithGuestId -import com.twitter.timelines.configapi.WithUserId - -case class RequestContext( - userId: Option[UserId], - guestId: Option[GuestId], - featureContext: FeatureContext = NullFeatureContext) - extends BaseRequestContext - with WithUserId - with WithGuestId - with WithFeatureContext diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/RequestContextFactory.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/RequestContextFactory.scala deleted file mode 100644 index 89d8617a3..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/RequestContextFactory.scala +++ /dev/null @@ -1,74 +0,0 @@ -package com.twitter.follow_recommendations.configapi - -import com.google.common.annotations.VisibleForTesting -import com.google.inject.Inject -import com.twitter.decider.Decider -import com.twitter.featureswitches.v2.FeatureSwitches -import com.twitter.featureswitches.{Recipient => FeatureSwitchRecipient} -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext -import com.twitter.snowflake.id.SnowflakeId -import com.twitter.timelines.configapi.FeatureContext -import com.twitter.timelines.configapi.FeatureValue -import com.twitter.timelines.configapi.ForcedFeatureContext -import com.twitter.timelines.configapi.OrElseFeatureContext -import com.twitter.timelines.configapi.featureswitches.v2.FeatureSwitchResultsFeatureContext -import javax.inject.Singleton - -/* - * Request Context Factory is used to build RequestContext objects which are used - * by the config api to determine the param overrides to apply to the request. - * The param overrides are determined per request by configs which specify which - * FS/Deciders/AB translate to what param overrides. - */ -@Singleton -class RequestContextFactory @Inject() (featureSwitches: FeatureSwitches, decider: Decider) { - def apply( - clientContext: ClientContext, - displayLocation: DisplayLocation, - featureOverrides: Map[String, FeatureValue] - ): RequestContext = { - val featureContext = getFeatureContext(clientContext, displayLocation, featureOverrides) - RequestContext(clientContext.userId, clientContext.guestId, featureContext) - } - - private[configapi] def getFeatureContext( - clientContext: ClientContext, - displayLocation: DisplayLocation, - featureOverrides: Map[String, FeatureValue] - ): FeatureContext = { - val recipient = - getFeatureSwitchRecipient(clientContext) - .withCustomFields("display_location" -> displayLocation.toFsName) - - // userAgeOpt is going to be set to None for logged out users and defaulted to Some(Int.MaxValue) for non-snowflake users - val userAgeOpt = clientContext.userId.map { userId => - SnowflakeId.timeFromIdOpt(userId).map(_.untilNow.inDays).getOrElse(Int.MaxValue) - } - val recipientWithAccountAge = - userAgeOpt - .map(age => recipient.withCustomFields("account_age_in_days" -> age)).getOrElse(recipient) - - val results = featureSwitches.matchRecipient(recipientWithAccountAge) - OrElseFeatureContext( - ForcedFeatureContext(featureOverrides), - new FeatureSwitchResultsFeatureContext(results)) - } - - @VisibleForTesting - private[configapi] def getFeatureSwitchRecipient( - clientContext: ClientContext - ): FeatureSwitchRecipient = { - FeatureSwitchRecipient( - userId = clientContext.userId, - userRoles = clientContext.userRoles, - deviceId = clientContext.deviceId, - guestId = clientContext.guestId, - languageCode = clientContext.languageCode, - countryCode = clientContext.countryCode, - isVerified = None, - clientApplicationId = clientContext.appId, - isTwoffice = clientContext.isTwoffice - ) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/BUILD deleted file mode 100644 index 5470c9bf4..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/BUILD +++ /dev/null @@ -1,18 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "configapi/configapi-core", - "configapi/configapi-decider", - "configapi/configapi-featureswitches:v2", - "featureswitches/featureswitches-core", - "featureswitches/featureswitches-core:v2", - "featureswitches/featureswitches-core/src/main/scala/com/twitter/featureswitches/v2/builder", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserContext.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserContext.scala deleted file mode 100644 index 6b954a911..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserContext.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.twitter.follow_recommendations.configapi.candidates - -import com.twitter.timelines.configapi.BaseRequestContext -import com.twitter.timelines.configapi.FeatureContext -import com.twitter.timelines.configapi.NullFeatureContext -import com.twitter.timelines.configapi.WithFeatureContext -import com.twitter.timelines.configapi.WithUserId - -/** - * represent the context for a recommendation candidate (producer side) - * @param userId id of the recommended user - * @param featureContext feature context - */ -case class CandidateUserContext( - override val userId: Option[Long], - featureContext: FeatureContext = NullFeatureContext) - extends BaseRequestContext - with WithUserId - with WithFeatureContext diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserContextFactory.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserContextFactory.scala deleted file mode 100644 index 4f30cf17a..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserContextFactory.scala +++ /dev/null @@ -1,55 +0,0 @@ -package com.twitter.follow_recommendations.configapi.candidates - -import com.google.common.annotations.VisibleForTesting -import com.google.inject.Inject -import com.twitter.decider.Decider -import com.twitter.featureswitches.v2.FeatureSwitches -import com.twitter.featureswitches.{Recipient => FeatureSwitchRecipient} -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants.PRODUCER_SIDE_FEATURE_SWITCHES -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.timelines.configapi.FeatureContext -import com.twitter.timelines.configapi.featureswitches.v2.FeatureSwitchResultsFeatureContext -import javax.inject.Named -import javax.inject.Singleton - -@Singleton -class CandidateUserContextFactory @Inject() ( - @Named(PRODUCER_SIDE_FEATURE_SWITCHES) featureSwitches: FeatureSwitches, - decider: Decider) { - def apply( - candidateUser: CandidateUser, - displayLocation: DisplayLocation - ): CandidateUserContext = { - val featureContext = getFeatureContext(candidateUser, displayLocation) - - CandidateUserContext(Some(candidateUser.id), featureContext) - } - - private[configapi] def getFeatureContext( - candidateUser: CandidateUser, - displayLocation: DisplayLocation - ): FeatureContext = { - - val recipient = getFeatureSwitchRecipient(candidateUser).withCustomFields( - "display_location" -> displayLocation.toFsName) - new FeatureSwitchResultsFeatureContext(featureSwitches.matchRecipient(recipient)) - } - - @VisibleForTesting - private[configapi] def getFeatureSwitchRecipient( - candidateUser: CandidateUser - ): FeatureSwitchRecipient = { - FeatureSwitchRecipient( - userId = Some(candidateUser.id), - userRoles = None, - deviceId = None, - guestId = None, - languageCode = None, - countryCode = None, - isVerified = None, - clientApplicationId = None, - isTwoffice = None - ) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserParamsFactory.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserParamsFactory.scala deleted file mode 100644 index 5afd09a63..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/CandidateUserParamsFactory.scala +++ /dev/null @@ -1,35 +0,0 @@ -package com.twitter.follow_recommendations.configapi.candidates - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.configapi.params.GlobalParams -import com.twitter.servo.util.MemoizingStatsReceiver -import com.twitter.timelines.configapi.Config -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Params -import javax.inject.Inject -import javax.inject.Singleton - -/** - * CandidateParamsFactory is primarily used for "producer side" experiments, don't use it on consumer side experiments - */ -@Singleton -class CandidateUserParamsFactory[T <: HasParams with HasDisplayLocation] @Inject() ( - config: Config, - candidateContextFactory: CandidateUserContextFactory, - statsReceiver: StatsReceiver) { - private val stats = new MemoizingStatsReceiver(statsReceiver.scope("configapi_candidate_params")) - def apply(candidateContext: CandidateUser, request: T): CandidateUser = { - if (candidateContext.params == Params.Invalid) { - if (request.params(GlobalParams.EnableCandidateParamHydrations)) { - candidateContext.copy(params = - config(candidateContextFactory(candidateContext, request.displayLocation), stats)) - } else { - candidateContext.copy(params = Params.Empty) - } - } else { - candidateContext - } - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/HydrateCandidateParamsTransform.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/HydrateCandidateParamsTransform.scala deleted file mode 100644 index deadd1d70..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates/HydrateCandidateParamsTransform.scala +++ /dev/null @@ -1,21 +0,0 @@ -package com.twitter.follow_recommendations.configapi.candidates - -import com.google.inject.Inject -import com.google.inject.Singleton -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.logging.Logging - -@Singleton -class HydrateCandidateParamsTransform[Target <: HasParams with HasDisplayLocation] @Inject() ( - candidateParamsFactory: CandidateUserParamsFactory[Target]) - extends Transform[Target, CandidateUser] - with Logging { - - def transform(target: Target, candidates: Seq[CandidateUser]): Stitch[Seq[CandidateUser]] = { - Stitch.value(candidates.map(candidateParamsFactory.apply(_, target))) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common/BUILD deleted file mode 100644 index 6fee24f89..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common/BUILD +++ /dev/null @@ -1,8 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "configapi/configapi-core", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common/FeatureSwitchConfig.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common/FeatureSwitchConfig.scala deleted file mode 100644 index 798b02670..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common/FeatureSwitchConfig.scala +++ /dev/null @@ -1,60 +0,0 @@ -package com.twitter.follow_recommendations.configapi.common - -import com.twitter.timelines.configapi.FeatureSwitchOverrideUtil.DefinedFeatureName -import com.twitter.timelines.configapi.FeatureSwitchOverrideUtil.ValueFeatureName -import com.twitter.timelines.configapi.BoundedParam -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.timelines.configapi.OptionalOverride -import com.twitter.timelines.configapi.Param -import com.twitter.util.Duration - -trait FeatureSwitchConfig { - def booleanFSParams: Seq[Param[Boolean] with FSName] = Nil - - def intFSParams: Seq[FSBoundedParam[Int]] = Nil - - def longFSParams: Seq[FSBoundedParam[Long]] = Nil - - def doubleFSParams: Seq[FSBoundedParam[Double]] = Nil - - def durationFSParams: Seq[FSBoundedParam[Duration] with HasDurationConversion] = Nil - - def optionalDoubleFSParams: Seq[ - (BoundedParam[Option[Double]], DefinedFeatureName, ValueFeatureName) - ] = Nil - - def stringSeqFSParams: Seq[Param[Seq[String]] with FSName] = Nil - - /** - * Apply overrides in list when the given FS Key is enabled. - * This override type does NOT work with experiments. Params here will be evaluated for every - * request IMMEDIATELY, not upon param.apply. If you would like to use an experiment pls use - * the primitive type or ENUM overrides. - */ - def gatedOverridesMap: Map[String, Seq[OptionalOverride[_]]] = Map.empty -} - -object FeatureSwitchConfig { - def merge(configs: Seq[FeatureSwitchConfig]): FeatureSwitchConfig = new FeatureSwitchConfig { - override def booleanFSParams: Seq[Param[Boolean] with FSName] = - configs.flatMap(_.booleanFSParams) - override def intFSParams: Seq[FSBoundedParam[Int]] = - configs.flatMap(_.intFSParams) - override def longFSParams: Seq[FSBoundedParam[Long]] = - configs.flatMap(_.longFSParams) - override def durationFSParams: Seq[FSBoundedParam[Duration] with HasDurationConversion] = - configs.flatMap(_.durationFSParams) - override def gatedOverridesMap: Map[String, Seq[OptionalOverride[_]]] = - configs.flatMap(_.gatedOverridesMap).toMap - override def doubleFSParams: Seq[FSBoundedParam[Double]] = - configs.flatMap(_.doubleFSParams) - override def optionalDoubleFSParams: Seq[ - (BoundedParam[Option[Double]], DefinedFeatureName, ValueFeatureName) - ] = - configs.flatMap(_.optionalDoubleFSParams) - override def stringSeqFSParams: Seq[Param[Seq[String]] with FSName] = - configs.flatMap(_.stringSeqFSParams) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/BUILD deleted file mode 100644 index e4982dc0f..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/BUILD +++ /dev/null @@ -1,10 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "configapi/configapi-core", - "configapi/configapi-decider", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/DeciderKey.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/DeciderKey.scala deleted file mode 100644 index f4c069a63..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/DeciderKey.scala +++ /dev/null @@ -1,51 +0,0 @@ -package com.twitter.follow_recommendations.configapi.deciders - -import com.twitter.servo.decider.DeciderKeyEnum - -object DeciderKey extends DeciderKeyEnum { - val EnableDiffyModuleDarkReading = Value("enable_diffy_module_dark_reading") - val EnableRecommendations = Value("enable_recommendations") - val EnableScoreUserCandidates = Value("enable_score_user_candidates") - val EnableProfileSidebarProduct = Value("enable_profile_sidebar_product") - val EnableMagicRecsProduct = Value("enable_magic_recs_product") - val EnableRuxLandingPageProduct = Value("enable_rux_landing_page_product") - val EnableRuxPymkProduct = Value("enable_rux_pymk_product") - val EnableProfileBonusFollowProduct = Value("enable_profile_bonus_follow_product") - val EnableElectionExploreWtfProduct = Value("enable_election_explore_wtf_product") - val EnableClusterFollowProduct = Value("enable_cluster_follow_product") - val EnableHomeTimelineProduct = Value("enable_home_timeline_product") - val EnableHtlBonusFollowProduct = Value("enable_htl_bonus_follow_product") - val EnableExploreTabProduct = Value("enable_explore_tab_product") - val EnableSidebarProduct = Value("enable_sidebar_product") - val EnableNuxPymkProduct = Value("enable_nux_pymk_product") - val EnableNuxInterestsProduct = Value("enable_nux_interests_product") - val EnableNuxTopicBonusFollowProduct = Value("enable_nux_topic_bonus_follow_product") - val EnableCampaignFormProduct = Value("enable_campaign_form_product") - val EnableReactiveFollowProduct = Value("enable_reactive_follow_product") - val EnableIndiaCovid19CuratedAccountsWtfProduct = Value( - "enable_india_covid19_curated_accounts_wtf_product") - val EnableAbUploadProduct = Value("enable_ab_upload_product") - val EnablePeolePlusPlusProduct = Value("enable_people_plus_plus_product") - val EnableTweetNotificationRecsProduct = Value("enable_tweet_notification_recs_product") - val EnableProfileDeviceFollow = Value("enable_profile_device_follow_product") - val EnableRecosBackfillProduct = Value("enable_recos_backfill_product") - val EnablePostNuxFollowTaskProduct = Value("enable_post_nux_follow_task_product") - val EnableCuratedSpaceHostsProduct = Value("enable_curated_space_hosts_product") - val EnableNuxGeoCategoryProduct = Value("enable_nux_geo_category_product") - val EnableNuxInterestsCategoryProduct = Value("enable_nux_interests_category_product") - val EnableNuxPymkCategoryProduct = Value("enable_nux_pymk_category_product") - val EnableHomeTimelineTweetRecsProduct = Value("enable_home_timeline_tweet_recs_product") - val EnableHtlBulkFriendFollowsProduct = Value("enable_htl_bulk_friend_follows_product") - val EnableNuxAutoFollowProduct = Value("enable_nux_auto_follow_product") - val EnableSearchBonusFollowProduct = Value("enable_search_bonus_follow_product") - val EnableFetchUserInRequestBuilder = Value("enable_fetch_user_in_request_builder") - val EnableProductMixerMagicRecsProduct = Value("enable_product_mixer_magic_recs_product") - val EnableHomeTimelineReverseChronProduct = Value("enable_home_timeline_reverse_chron_product") - val EnableProductMixerPipelineMagicRecsDarkRead = Value( - "enable_product_mixer_pipeline_magic_recs_dark_read") - val EnableExperimentalCaching = Value("enable_experimental_caching") - val EnableDistributedCaching = Value("enable_distributed_caching") - val EnableGizmoduckCaching = Value("enable_gizmoduck_caching") - val EnableTrafficDarkReading = Value("enable_traffic_dark_reading") - val EnableGraphFeatureServiceRequests = Value("enable_graph_feature_service_requests") -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/DeciderParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/DeciderParams.scala deleted file mode 100644 index 07bf3e14d..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders/DeciderParams.scala +++ /dev/null @@ -1,8 +0,0 @@ -package com.twitter.follow_recommendations.configapi.deciders - -import com.twitter.timelines.configapi.Param - -object DeciderParams { - object EnableRecommendations extends Param[Boolean](false) - object EnableScoreUserCandidates extends Param[Boolean](false) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params/BUILD deleted file mode 100644 index 1bb357b2c..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "configapi/configapi-core", - "configapi/configapi-decider", - "configapi/configapi-featureswitches:v2", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params/GlobalParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params/GlobalParams.scala deleted file mode 100644 index cf1905c41..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params/GlobalParams.scala +++ /dev/null @@ -1,35 +0,0 @@ -package com.twitter.follow_recommendations.configapi.params - -import com.twitter.follow_recommendations.models.CandidateSourceType -import com.twitter.timelines.configapi.FSEnumParam -import com.twitter.timelines.configapi.FSParam - -/** - * When adding Producer side experiments, make sure to register the FS Key in [[ProducerFeatureFilter]] - * in [[FeatureSwitchesModule]], otherwise, the FS will not work. - */ -object GlobalParams { - - object EnableCandidateParamHydrations - extends FSParam[Boolean]("frs_receiver_enable_candidate_params", false) - - object KeepUserCandidate - extends FSParam[Boolean]("frs_receiver_holdback_keep_user_candidate", true) - - object KeepSocialUserCandidate - extends FSParam[Boolean]("frs_receiver_holdback_keep_social_user_candidate", true) - - case object EnableGFSSocialProofTransform - extends FSParam("social_proof_transform_use_graph_feature_service", true) - - case object EnableWhoToFollowProducts extends FSParam("who_to_follow_product_enabled", true) - - case object CandidateSourcesToFilter - extends FSEnumParam[CandidateSourceType.type]( - "candidate_sources_type_filter_id", - CandidateSourceType.None, - CandidateSourceType) - - object EnableRecommendationFlowLogs - extends FSParam[Boolean]("frs_recommendation_flow_logs_enabled", false) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/BUILD deleted file mode 100644 index 2a625d856..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/BUILD +++ /dev/null @@ -1,29 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/javax/inject:javax.inject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "decider/src/main/scala", - "finagle/finagle-core/src/main", - "finatra/inject/inject-core/src/main/scala", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift:controller", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift/exceptions", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift/filters", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift/modules", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift/response", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift/routing", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services", - "follow-recommendations-service/thrift/src/main/thrift:thrift-scala", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/service/debug_query", - "scrooge/scrooge-core/src/main/scala", - "util/util-core:scala", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/CandidateUserDebugParamsBuilder.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/CandidateUserDebugParamsBuilder.scala deleted file mode 100644 index 1695c464d..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/CandidateUserDebugParamsBuilder.scala +++ /dev/null @@ -1,25 +0,0 @@ -package com.twitter.follow_recommendations.controllers - -import com.twitter.follow_recommendations.common.models._ -import com.twitter.follow_recommendations.configapi.ParamsFactory -import com.twitter.follow_recommendations.models.CandidateUserDebugParams -import com.twitter.follow_recommendations.models.FeatureValue -import com.twitter.follow_recommendations.{thriftscala => t} -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class CandidateUserDebugParamsBuilder @Inject() (paramsFactory: ParamsFactory) { - def fromThrift(req: t.ScoringUserRequest): CandidateUserDebugParams = { - val clientContext = ClientContextConverter.fromThrift(req.clientContext) - val displayLocation = DisplayLocation.fromThrift(req.displayLocation) - - CandidateUserDebugParams(req.candidates.map { candidate => - candidate.userId -> paramsFactory( - clientContext, - displayLocation, - candidate.featureOverrides - .map(_.mapValues(FeatureValue.fromThrift).toMap).getOrElse(Map.empty)) - }.toMap) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/RecommendationRequestBuilder.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/RecommendationRequestBuilder.scala deleted file mode 100644 index bc21fd6a3..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/RecommendationRequestBuilder.scala +++ /dev/null @@ -1,41 +0,0 @@ -package com.twitter.follow_recommendations.controllers - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.ClientContextConverter -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.models.DebugParams -import com.twitter.follow_recommendations.models.DisplayContext -import com.twitter.follow_recommendations.models.RecommendationRequest -import com.twitter.follow_recommendations.{thriftscala => t} -import com.twitter.gizmoduck.thriftscala.UserType -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RecommendationRequestBuilder @Inject() ( - requestBuilderUserFetcher: RequestBuilderUserFetcher, - statsReceiver: StatsReceiver) { - private val scopedStats = statsReceiver.scope(this.getClass.getSimpleName) - private val isSoftUserCounter = scopedStats.counter("is_soft_user") - - def fromThrift(tRequest: t.RecommendationRequest): Stitch[RecommendationRequest] = { - requestBuilderUserFetcher.fetchUser(tRequest.clientContext.userId).map { userOpt => - val isSoftUser = userOpt.exists(_.userType == UserType.Soft) - if (isSoftUser) isSoftUserCounter.incr() - RecommendationRequest( - clientContext = ClientContextConverter.fromThrift(tRequest.clientContext), - displayLocation = DisplayLocation.fromThrift(tRequest.displayLocation), - displayContext = tRequest.displayContext.map(DisplayContext.fromThrift), - maxResults = tRequest.maxResults, - cursor = tRequest.cursor, - excludedIds = tRequest.excludedIds, - fetchPromotedContent = tRequest.fetchPromotedContent, - debugParams = tRequest.debugParams.map(DebugParams.fromThrift), - userLocationState = tRequest.userLocationState, - isSoftUser = isSoftUser - ) - } - - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/RequestBuilderUserFetcher.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/RequestBuilderUserFetcher.scala deleted file mode 100644 index 3953b5ef3..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/RequestBuilderUserFetcher.scala +++ /dev/null @@ -1,48 +0,0 @@ -package com.twitter.follow_recommendations.controllers - -import com.twitter.decider.Decider -import com.twitter.decider.SimpleRecipient -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.follow_recommendations.configapi.deciders.DeciderKey -import com.twitter.gizmoduck.thriftscala.LookupContext -import com.twitter.gizmoduck.thriftscala.User -import com.twitter.stitch.Stitch -import com.twitter.stitch.gizmoduck.Gizmoduck -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RequestBuilderUserFetcher @Inject() ( - gizmoduck: Gizmoduck, - statsReceiver: StatsReceiver, - decider: Decider) { - private val scopedStats = statsReceiver.scope(this.getClass.getSimpleName) - - def fetchUser(userIdOpt: Option[Long]): Stitch[Option[User]] = { - userIdOpt match { - case Some(userId) if enableDecider(userId) => - val stitch = gizmoduck - .getUserById( - userId = userId, - context = LookupContext( - forUserId = Some(userId), - includeProtected = true, - includeSoftUsers = true - ) - ).map(user => Some(user)) - StatsUtil - .profileStitch(stitch, scopedStats) - .handle { - case _: Throwable => None - } - case _ => Stitch.None - } - } - - private def enableDecider(userId: Long): Boolean = { - decider.isAvailable( - DeciderKey.EnableFetchUserInRequestBuilder.toString, - Some(SimpleRecipient(userId))) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/ScoringUserRequestBuilder.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/ScoringUserRequestBuilder.scala deleted file mode 100644 index 4a45a19f7..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/ScoringUserRequestBuilder.scala +++ /dev/null @@ -1,53 +0,0 @@ -package com.twitter.follow_recommendations.controllers - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.ClientContextConverter -import com.twitter.follow_recommendations.common.models.DebugOptions -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.models.DebugParams -import com.twitter.follow_recommendations.models.ScoringUserRequest -import com.twitter.timelines.configapi.Params -import javax.inject.Inject -import javax.inject.Singleton -import com.twitter.follow_recommendations.{thriftscala => t} -import com.twitter.gizmoduck.thriftscala.UserType -import com.twitter.stitch.Stitch - -@Singleton -class ScoringUserRequestBuilder @Inject() ( - requestBuilderUserFetcher: RequestBuilderUserFetcher, - candidateUserDebugParamsBuilder: CandidateUserDebugParamsBuilder, - statsReceiver: StatsReceiver) { - private val scopedStats = statsReceiver.scope(this.getClass.getSimpleName) - private val isSoftUserCounter = scopedStats.counter("is_soft_user") - - def fromThrift(req: t.ScoringUserRequest): Stitch[ScoringUserRequest] = { - requestBuilderUserFetcher.fetchUser(req.clientContext.userId).map { userOpt => - val isSoftUser = userOpt.exists(_.userType == UserType.Soft) - if (isSoftUser) isSoftUserCounter.incr() - - val candidateUsersParamsMap = candidateUserDebugParamsBuilder.fromThrift(req) - val candidates = req.candidates.map { candidate => - CandidateUser - .fromUserRecommendation(candidate).copy(params = - candidateUsersParamsMap.paramsMap.getOrElse(candidate.userId, Params.Invalid)) - } - - ScoringUserRequest( - clientContext = ClientContextConverter.fromThrift(req.clientContext), - displayLocation = DisplayLocation.fromThrift(req.displayLocation), - params = Params.Empty, - debugOptions = req.debugParams.map(DebugOptions.fromDebugParamsThrift), - recentFollowedUserIds = None, - recentFollowedByUserIds = None, - wtfImpressions = None, - similarToUserIds = Nil, - candidates = candidates, - debugParams = req.debugParams.map(DebugParams.fromThrift), - isSoftUser = isSoftUser - ) - } - } - -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/ThriftController.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/ThriftController.scala deleted file mode 100644 index f3014982e..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/controllers/ThriftController.scala +++ /dev/null @@ -1,41 +0,0 @@ -package com.twitter.follow_recommendations.controllers - -import com.twitter.finatra.thrift.Controller -import com.twitter.follow_recommendations.configapi.ParamsFactory -import com.twitter.follow_recommendations.services.ProductPipelineSelector -import com.twitter.follow_recommendations.services.UserScoringService -import com.twitter.follow_recommendations.thriftscala.FollowRecommendationsThriftService -import com.twitter.follow_recommendations.thriftscala.FollowRecommendationsThriftService._ -import com.twitter.stitch.Stitch -import javax.inject.Inject - -class ThriftController @Inject() ( - userScoringService: UserScoringService, - recommendationRequestBuilder: RecommendationRequestBuilder, - scoringUserRequestBuilder: ScoringUserRequestBuilder, - productPipelineSelector: ProductPipelineSelector, - paramsFactory: ParamsFactory) - extends Controller(FollowRecommendationsThriftService) { - - handle(GetRecommendations) { args: GetRecommendations.Args => - val stitch = recommendationRequestBuilder.fromThrift(args.request).flatMap { request => - val params = paramsFactory( - request.clientContext, - request.displayLocation, - request.debugParams.flatMap(_.featureOverrides).getOrElse(Map.empty)) - productPipelineSelector.selectPipeline(request, params).map(_.toThrift) - } - Stitch.run(stitch) - } - - handle(ScoreUserCandidates) { args: ScoreUserCandidates.Args => - val stitch = scoringUserRequestBuilder.fromThrift(args.request).flatMap { request => - val params = paramsFactory( - request.clientContext, - request.displayLocation, - request.debugParams.flatMap(_.featureOverrides).getOrElse(Map.empty)) - userScoringService.get(request.copy(params = params)).map(_.toThrift) - } - Stitch.run(stitch) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/BUILD deleted file mode 100644 index aa775c6a7..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/BUILD +++ /dev/null @@ -1,19 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/promoted_accounts", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/tracking_token", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlow.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlow.scala deleted file mode 100644 index dd9372484..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlow.scala +++ /dev/null @@ -1,112 +0,0 @@ -package com.twitter.follow_recommendations.flows.ads - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.EnrichedCandidateSource -import com.twitter.follow_recommendations.common.base.IdentityRanker -import com.twitter.follow_recommendations.common.base.IdentityTransform -import com.twitter.follow_recommendations.common.base.ParamPredicate -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.Ranker -import com.twitter.follow_recommendations.common.base.RecommendationFlow -import com.twitter.follow_recommendations.common.base.RecommendationResultsConfig -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.follow_recommendations.common.base.TruePredicate -import com.twitter.follow_recommendations.common.candidate_sources.promoted_accounts.PromotedAccountsCandidateSource -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.predicates.ExcludedUserIdPredicate -import com.twitter.follow_recommendations.common.transforms.tracking_token.TrackingTokenTransform -import com.twitter.inject.annotations.Flag -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.util.Duration -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PromotedAccountsFlow @Inject() ( - promotedAccountsCandidateSource: PromotedAccountsCandidateSource, - trackingTokenTransform: TrackingTokenTransform, - baseStatsReceiver: StatsReceiver, - @Flag("fetch_prod_promoted_accounts") fetchProductionPromotedAccounts: Boolean) - extends RecommendationFlow[PromotedAccountsFlowRequest, CandidateUser] { - - protected override def targetEligibility: Predicate[PromotedAccountsFlowRequest] = - new ParamPredicate[PromotedAccountsFlowRequest]( - PromotedAccountsFlowParams.TargetEligibility - ) - - protected override def candidateSources( - target: PromotedAccountsFlowRequest - ): Seq[CandidateSource[PromotedAccountsFlowRequest, CandidateUser]] = { - import EnrichedCandidateSource._ - val candidateSourceStats = statsReceiver.scope("candidate_sources") - val budget: Duration = target.params(PromotedAccountsFlowParams.FetchCandidateSourceBudget) - val candidateSources = Seq( - promotedAccountsCandidateSource - .mapKeys[PromotedAccountsFlowRequest](r => - Seq(r.toAdsRequest(fetchProductionPromotedAccounts))) - .mapValue(PromotedAccountsUtil.toCandidateUser) - ).map { candidateSource => - candidateSource - .failOpenWithin(budget, candidateSourceStats).observe(candidateSourceStats) - } - candidateSources - } - - protected override def preRankerCandidateFilter: Predicate[ - (PromotedAccountsFlowRequest, CandidateUser) - ] = { - val preRankerFilterStats = statsReceiver.scope("pre_ranker") - ExcludedUserIdPredicate.observe(preRankerFilterStats.scope("exclude_user_id_predicate")) - } - - /** - * rank the candidates - */ - protected override def selectRanker( - target: PromotedAccountsFlowRequest - ): Ranker[PromotedAccountsFlowRequest, CandidateUser] = { - new IdentityRanker[PromotedAccountsFlowRequest, CandidateUser] - } - - /** - * transform the candidates after ranking (e.g. dedupping, grouping and etc) - */ - protected override def postRankerTransform: Transform[ - PromotedAccountsFlowRequest, - CandidateUser - ] = { - new IdentityTransform[PromotedAccountsFlowRequest, CandidateUser] - } - - /** - * filter invalid candidates before returning the results. - * - * Some heavy filters e.g. SGS filter could be applied in this step - */ - protected override def validateCandidates: Predicate[ - (PromotedAccountsFlowRequest, CandidateUser) - ] = { - new TruePredicate[(PromotedAccountsFlowRequest, CandidateUser)] - } - - /** - * transform the candidates into results and return - */ - protected override def transformResults: Transform[PromotedAccountsFlowRequest, CandidateUser] = { - trackingTokenTransform - } - - /** - * configuration for recommendation results - */ - protected override def resultsConfig( - target: PromotedAccountsFlowRequest - ): RecommendationResultsConfig = { - RecommendationResultsConfig( - target.params(PromotedAccountsFlowParams.ResultSizeParam), - target.params(PromotedAccountsFlowParams.BatchSizeParam) - ) - } - - override val statsReceiver: StatsReceiver = baseStatsReceiver.scope("promoted_accounts_flow") -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlowParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlowParams.scala deleted file mode 100644 index 010aea509..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlowParams.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.twitter.follow_recommendations.flows.ads -import com.twitter.conversions.DurationOps._ -import com.twitter.timelines.configapi.Param -import com.twitter.util.Duration - -abstract class PromotedAccountsFlowParams[A](default: A) extends Param[A](default) { - override val statName: String = "ads/" + this.getClass.getSimpleName -} - -object PromotedAccountsFlowParams { - - // number of total slots returned to the end user, available to put ads - case object TargetEligibility extends PromotedAccountsFlowParams[Boolean](true) - case object ResultSizeParam extends PromotedAccountsFlowParams[Int](Int.MaxValue) - case object BatchSizeParam extends PromotedAccountsFlowParams[Int](Int.MaxValue) - case object FetchCandidateSourceBudget - extends PromotedAccountsFlowParams[Duration](1000.millisecond) - -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlowRequest.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlowRequest.scala deleted file mode 100644 index 61afb3049..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsFlowRequest.scala +++ /dev/null @@ -1,33 +0,0 @@ -package com.twitter.follow_recommendations.flows.ads -import com.twitter.follow_recommendations.common.clients.adserver.AdRequest -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasExcludedUserIds -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Params - -case class PromotedAccountsFlowRequest( - override val clientContext: ClientContext, - override val params: Params, - displayLocation: DisplayLocation, - profileId: Option[Long], - // note we also add userId and profileId to excludeUserIds - excludeIds: Seq[Long]) - extends HasParams - with HasClientContext - with HasExcludedUserIds - with HasDisplayLocation { - def toAdsRequest(fetchProductionPromotedAccounts: Boolean): AdRequest = { - AdRequest( - clientContext = clientContext, - displayLocation = displayLocation, - isTest = Some(!fetchProductionPromotedAccounts), - profileUserId = profileId - ) - } - override val excludedUserIds: Seq[Long] = { - excludeIds ++ clientContext.userId.toSeq ++ profileId.toSeq - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsUtil.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsUtil.scala deleted file mode 100644 index 5327911c2..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads/PromotedAccountsUtil.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.twitter.follow_recommendations.flows.ads -import com.twitter.follow_recommendations.common.candidate_sources.promoted_accounts.PromotedCandidateUser -import com.twitter.follow_recommendations.common.models.AccountProof -import com.twitter.follow_recommendations.common.models.AdMetadata -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.Reason -import com.twitter.follow_recommendations.common.models.UserCandidateSourceDetails - -object PromotedAccountsUtil { - def toCandidateUser(promotedCandidateUser: PromotedCandidateUser): CandidateUser = { - CandidateUser( - id = promotedCandidateUser.id, - score = None, - adMetadata = - Some(AdMetadata(promotedCandidateUser.position, promotedCandidateUser.adImpression)), - reason = Some( - Reason( - accountProof = Some(AccountProof(followProof = Some(promotedCandidateUser.followProof)))) - ), - userCandidateSourceDetails = Some( - UserCandidateSourceDetails( - promotedCandidateUser.primaryCandidateSource, - Map.empty, - Map.empty, - None)) - ) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/BUILD deleted file mode 100644 index 886c0fe5a..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/BUILD +++ /dev/null @@ -1,32 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/top_organic_follows_accounts", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/dedup", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/tracking_token", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlow.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlow.scala deleted file mode 100644 index 30dfa0d42..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlow.scala +++ /dev/null @@ -1,202 +0,0 @@ -package com.twitter.follow_recommendations.flows.content_recommender_flow - -import com.twitter.conversions.DurationOps._ -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.EnrichedCandidateSource -import com.twitter.follow_recommendations.common.base.GatedPredicateBase -import com.twitter.follow_recommendations.common.base.ParamPredicate -import com.twitter.follow_recommendations.common.base.Predicate -import com.twitter.follow_recommendations.common.base.Ranker -import com.twitter.follow_recommendations.common.base.RecommendationFlow -import com.twitter.follow_recommendations.common.base.RecommendationResultsConfig -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.predicates.ExcludedUserIdPredicate -import com.twitter.follow_recommendations.common.predicates.InactivePredicate -import com.twitter.follow_recommendations.common.predicates.gizmoduck.GizmoduckPredicate -import com.twitter.follow_recommendations.common.predicates.sgs.InvalidRelationshipPredicate -import com.twitter.follow_recommendations.common.predicates.sgs.InvalidTargetCandidateRelationshipTypesPredicate -import com.twitter.follow_recommendations.common.predicates.sgs.RecentFollowingPredicate -import com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker.WeightedCandidateSourceRanker -import com.twitter.follow_recommendations.common.transforms.dedup.DedupTransform -import com.twitter.follow_recommendations.common.transforms.tracking_token.TrackingTokenTransform -import com.twitter.follow_recommendations.utils.CandidateSourceHoldbackUtil -import com.twitter.follow_recommendations.utils.RecommendationFlowBaseSideEffectsUtil -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.quality_factor.BoundsWithDefault -import com.twitter.product_mixer.core.quality_factor.LinearLatencyQualityFactor -import com.twitter.product_mixer.core.quality_factor.LinearLatencyQualityFactorConfig -import com.twitter.product_mixer.core.quality_factor.LinearLatencyQualityFactorObserver -import com.twitter.product_mixer.core.quality_factor.QualityFactorObserver - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ContentRecommenderFlow @Inject() ( - contentRecommenderFlowCandidateSourceRegistry: ContentRecommenderFlowCandidateSourceRegistry, - recentFollowingPredicate: RecentFollowingPredicate, - gizmoduckPredicate: GizmoduckPredicate, - inactivePredicate: InactivePredicate, - sgsPredicate: InvalidTargetCandidateRelationshipTypesPredicate, - invalidRelationshipPredicate: InvalidRelationshipPredicate, - trackingTokenTransform: TrackingTokenTransform, - baseStatsReceiver: StatsReceiver) - extends RecommendationFlow[ContentRecommenderRequest, CandidateUser] - with RecommendationFlowBaseSideEffectsUtil[ContentRecommenderRequest, CandidateUser] - with CandidateSourceHoldbackUtil { - - override val statsReceiver: StatsReceiver = baseStatsReceiver.scope("content_recommender_flow") - - override val qualityFactorObserver: Option[QualityFactorObserver] = { - val config = LinearLatencyQualityFactorConfig( - qualityFactorBounds = - BoundsWithDefault(minInclusive = 0.1, maxInclusive = 1.0, default = 1.0), - initialDelay = 60.seconds, - targetLatency = 100.milliseconds, - targetLatencyPercentile = 95.0, - delta = 0.001 - ) - val qualityFactor = LinearLatencyQualityFactor(config) - val observer = LinearLatencyQualityFactorObserver(qualityFactor) - statsReceiver.provideGauge("quality_factor")(qualityFactor.currentValue.toFloat) - Some(observer) - } - - protected override def targetEligibility: Predicate[ContentRecommenderRequest] = - new ParamPredicate[ContentRecommenderRequest]( - ContentRecommenderParams.TargetEligibility - ) - - protected override def candidateSources( - target: ContentRecommenderRequest - ): Seq[CandidateSource[ContentRecommenderRequest, CandidateUser]] = { - import EnrichedCandidateSource._ - val identifiers = ContentRecommenderFlowCandidateSourceWeights.getWeights(target.params).keySet - val selected = contentRecommenderFlowCandidateSourceRegistry.select(identifiers) - val budget = - target.params(ContentRecommenderParams.FetchCandidateSourceBudgetInMillisecond).millisecond - filterCandidateSources(target, selected.map(c => c.failOpenWithin(budget, statsReceiver)).toSeq) - } - - protected override val preRankerCandidateFilter: Predicate[ - (ContentRecommenderRequest, CandidateUser) - ] = { - val preRankerFilterStats = statsReceiver.scope("pre_ranker") - val recentFollowingPredicateStats = preRankerFilterStats.scope("recent_following_predicate") - val invalidRelationshipPredicateStats = - preRankerFilterStats.scope("invalid_relationship_predicate") - - object recentFollowingGatedPredicate - extends GatedPredicateBase[(ContentRecommenderRequest, CandidateUser)]( - recentFollowingPredicate, - recentFollowingPredicateStats - ) { - override def gate(item: (ContentRecommenderRequest, CandidateUser)): Boolean = - item._1.params(ContentRecommenderParams.EnableRecentFollowingPredicate) - } - - object invalidRelationshipGatedPredicate - extends GatedPredicateBase[(ContentRecommenderRequest, CandidateUser)]( - invalidRelationshipPredicate, - invalidRelationshipPredicateStats - ) { - override def gate(item: (ContentRecommenderRequest, CandidateUser)): Boolean = - item._1.params(ContentRecommenderParams.EnableInvalidRelationshipPredicate) - } - - ExcludedUserIdPredicate - .observe(preRankerFilterStats.scope("exclude_user_id_predicate")) - .andThen(recentFollowingGatedPredicate.observe(recentFollowingPredicateStats)) - .andThen(invalidRelationshipGatedPredicate.observe(invalidRelationshipPredicateStats)) - } - - /** - * rank the candidates - */ - protected override def selectRanker( - target: ContentRecommenderRequest - ): Ranker[ContentRecommenderRequest, CandidateUser] = { - val rankersStatsReceiver = statsReceiver.scope("rankers") - WeightedCandidateSourceRanker - .build[ContentRecommenderRequest]( - ContentRecommenderFlowCandidateSourceWeights.getWeights(target.params), - randomSeed = target.getRandomizationSeed - ).observe(rankersStatsReceiver.scope("weighted_candidate_source_ranker")) - } - - /** - * transform the candidates after ranking - */ - protected override def postRankerTransform: Transform[ - ContentRecommenderRequest, - CandidateUser - ] = { - new DedupTransform[ContentRecommenderRequest, CandidateUser] - .observe(statsReceiver.scope("dedupping")) - } - - protected override def validateCandidates: Predicate[ - (ContentRecommenderRequest, CandidateUser) - ] = { - val stats = statsReceiver.scope("validate_candidates") - val gizmoduckPredicateStats = stats.scope("gizmoduck_predicate") - val inactivePredicateStats = stats.scope("inactive_predicate") - val sgsPredicateStats = stats.scope("sgs_predicate") - - val includeGizmoduckPredicate = - new ParamPredicate[ContentRecommenderRequest]( - ContentRecommenderParams.EnableGizmoduckPredicate) - .map[(ContentRecommenderRequest, CandidateUser)] { - case (request: ContentRecommenderRequest, _) => - request - } - - val includeInactivePredicate = - new ParamPredicate[ContentRecommenderRequest]( - ContentRecommenderParams.EnableInactivePredicate) - .map[(ContentRecommenderRequest, CandidateUser)] { - case (request: ContentRecommenderRequest, _) => - request - } - - val includeInvalidTargetCandidateRelationshipTypesPredicate = - new ParamPredicate[ContentRecommenderRequest]( - ContentRecommenderParams.EnableInvalidTargetCandidateRelationshipPredicate) - .map[(ContentRecommenderRequest, CandidateUser)] { - case (request: ContentRecommenderRequest, _) => - request - } - - Predicate - .andConcurrently[(ContentRecommenderRequest, CandidateUser)]( - Seq( - gizmoduckPredicate.observe(gizmoduckPredicateStats).gate(includeGizmoduckPredicate), - inactivePredicate.observe(inactivePredicateStats).gate(includeInactivePredicate), - sgsPredicate - .observe(sgsPredicateStats).gate( - includeInvalidTargetCandidateRelationshipTypesPredicate), - ) - ) - } - - /** - * transform the candidates into results and return - */ - protected override def transformResults: Transform[ContentRecommenderRequest, CandidateUser] = { - trackingTokenTransform - } - - /** - * configuration for recommendation results - */ - protected override def resultsConfig( - target: ContentRecommenderRequest - ): RecommendationResultsConfig = { - RecommendationResultsConfig( - target.maxResults.getOrElse(target.params(ContentRecommenderParams.ResultSizeParam)), - target.params(ContentRecommenderParams.BatchSizeParam) - ) - } - -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceRegistry.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceRegistry.scala deleted file mode 100644 index 4a6c61042..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceRegistry.scala +++ /dev/null @@ -1,78 +0,0 @@ -package com.twitter.follow_recommendations.flows.content_recommender_flow - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.CandidateSourceRegistry -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ForwardEmailBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ForwardPhoneBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ReverseEmailBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ReversePhoneBookSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopCountryBackFillSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopCountrySource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeohashSource -import com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts.CrowdSearchAccountsSource -import com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow.PPMILocaleFollowSource -import com.twitter.follow_recommendations.common.candidate_sources.top_organic_follows_accounts.TopOrganicFollowsAccountsSource -import com.twitter.follow_recommendations.common.candidate_sources.real_graph.RealGraphOonV2Source -import com.twitter.follow_recommendations.common.candidate_sources.recent_engagement.RepeatedProfileVisitsSource -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentEngagementSimilarUsersSource -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentFollowingSimilarUsersSource -import com.twitter.follow_recommendations.common.candidate_sources.socialgraph.RecentFollowingRecentFollowingExpansionSource -import com.twitter.follow_recommendations.common.candidate_sources.stp.OfflineStrongTiePredictionSource -import com.twitter.follow_recommendations.common.candidate_sources.triangular_loops.TriangularLoopsSource -import com.twitter.follow_recommendations.common.candidate_sources.user_user_graph.UserUserGraphCandidateSource -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ContentRecommenderFlowCandidateSourceRegistry @Inject() ( - // social based - forwardPhoneBookSource: ForwardPhoneBookSource, - forwardEmailBookSource: ForwardEmailBookSource, - reversePhoneBookSource: ReversePhoneBookSource, - reverseEmailBookSource: ReverseEmailBookSource, - offlineStrongTiePredictionSource: OfflineStrongTiePredictionSource, - triangularLoopsSource: TriangularLoopsSource, - userUserGraphCandidateSource: UserUserGraphCandidateSource, - realGraphOonSource: RealGraphOonV2Source, - recentFollowingRecentFollowingExpansionSource: RecentFollowingRecentFollowingExpansionSource, - // activity based - recentFollowingSimilarUsersSource: RecentFollowingSimilarUsersSource, - recentEngagementSimilarUsersSource: RecentEngagementSimilarUsersSource, - repeatedProfileVisitsSource: RepeatedProfileVisitsSource, - // geo based - popCountrySource: PopCountrySource, - popGeohashSource: PopGeohashSource, - popCountryBackFillSource: PopCountryBackFillSource, - crowdSearchAccountsSource: CrowdSearchAccountsSource, - topOrganicFollowsAccountsSource: TopOrganicFollowsAccountsSource, - ppmiLocaleFollowSource: PPMILocaleFollowSource, - baseStatsReceiver: StatsReceiver) - extends CandidateSourceRegistry[ContentRecommenderRequest, CandidateUser] { - - override val statsReceiver = baseStatsReceiver - .scope("content_recommender_flow", "candidate_sources") - - override val sources: Set[CandidateSource[ContentRecommenderRequest, CandidateUser]] = Seq( - forwardPhoneBookSource, - forwardEmailBookSource, - reversePhoneBookSource, - reverseEmailBookSource, - offlineStrongTiePredictionSource, - triangularLoopsSource, - userUserGraphCandidateSource, - realGraphOonSource, - recentFollowingRecentFollowingExpansionSource, - recentFollowingSimilarUsersSource, - recentEngagementSimilarUsersSource, - repeatedProfileVisitsSource, - popCountrySource, - popGeohashSource, - popCountryBackFillSource, - crowdSearchAccountsSource, - topOrganicFollowsAccountsSource, - ppmiLocaleFollowSource, - ).toSet -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceWeights.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceWeights.scala deleted file mode 100644 index 845a6ec0a..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceWeights.scala +++ /dev/null @@ -1,71 +0,0 @@ -package com.twitter.follow_recommendations.flows.content_recommender_flow - -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ForwardEmailBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ForwardPhoneBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ReverseEmailBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ReversePhoneBookSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopCountryBackFillSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopCountrySource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeohashSource -import com.twitter.follow_recommendations.common.candidate_sources.real_graph.RealGraphOonV2Source -import com.twitter.follow_recommendations.common.candidate_sources.recent_engagement.RepeatedProfileVisitsSource -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentEngagementSimilarUsersSource -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentFollowingSimilarUsersSource -import com.twitter.follow_recommendations.common.candidate_sources.stp.OfflineStrongTiePredictionSource -import com.twitter.follow_recommendations.common.candidate_sources.triangular_loops.TriangularLoopsSource -import com.twitter.follow_recommendations.common.candidate_sources.user_user_graph.UserUserGraphCandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts.CrowdSearchAccountsSource -import com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow.PPMILocaleFollowSource -import com.twitter.follow_recommendations.common.candidate_sources.socialgraph.RecentFollowingRecentFollowingExpansionSource -import com.twitter.follow_recommendations.common.candidate_sources.top_organic_follows_accounts.TopOrganicFollowsAccountsSource -import com.twitter.timelines.configapi.Params - -object ContentRecommenderFlowCandidateSourceWeights { - - def getWeights( - params: Params - ): Map[CandidateSourceIdentifier, Double] = { - Map[CandidateSourceIdentifier, Double]( - // Social based - UserUserGraphCandidateSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.UserUserGraphSourceWeight), - ForwardPhoneBookSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.ForwardPhoneBookSourceWeight), - ReversePhoneBookSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.ReversePhoneBookSourceWeight), - ForwardEmailBookSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.ForwardEmailBookSourceWeight), - ReverseEmailBookSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.ReverseEmailBookSourceWeight), - TriangularLoopsSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.TriangularLoopsSourceWeight), - OfflineStrongTiePredictionSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.OfflineStrongTiePredictionSourceWeight), - RecentFollowingRecentFollowingExpansionSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.NewFollowingNewFollowingExpansionSourceWeight), - RecentFollowingSimilarUsersSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.NewFollowingSimilarUserSourceWeight), - // Activity based - RealGraphOonV2Source.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.RealGraphOonSourceWeight), - RecentEngagementSimilarUsersSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.RecentEngagementSimilarUserSourceWeight), - RepeatedProfileVisitsSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.RepeatedProfileVisitsSourceWeight), - // Geo based - PopCountrySource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.PopCountrySourceWeight), - PopGeohashSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.PopGeohashSourceWeight), - PopCountryBackFillSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.PopCountryBackfillSourceWeight), - PPMILocaleFollowSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.PPMILocaleFollowSourceWeight), - CrowdSearchAccountsSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.CrowdSearchAccountSourceWeight), - TopOrganicFollowsAccountsSource.Identifier -> params( - ContentRecommenderFlowCandidateSourceWeightsParams.TopOrganicFollowsAccountsSourceWeight), - ) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceWeightsParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceWeightsParams.scala deleted file mode 100644 index 462de260b..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowCandidateSourceWeightsParams.scala +++ /dev/null @@ -1,117 +0,0 @@ -package com.twitter.follow_recommendations.flows.content_recommender_flow - -import com.twitter.timelines.configapi.FSBoundedParam - -object ContentRecommenderFlowCandidateSourceWeightsParams { - // Social based - case object ForwardPhoneBookSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.ForwardPhoneBookSourceWeight, - 1d, - 0d, - 1000d) - case object ForwardEmailBookSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.ForwardEmailBookSourceWeight, - 1d, - 0d, - 1000d) - case object ReversePhoneBookSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.ReversePhoneBookSourceWeight, - 1d, - 0d, - 1000d) - case object ReverseEmailBookSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.ReverseEmailBookSourceWeight, - 1d, - 0d, - 1000d) - case object OfflineStrongTiePredictionSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.OfflineStrongTiePredictionSourceWeight, - 1d, - 0d, - 1000d) - case object TriangularLoopsSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.TriangularLoopsSourceWeight, - 1d, - 0d, - 1000d) - case object UserUserGraphSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.UserUserGraphSourceWeight, - 1d, - 0d, - 1000d) - case object NewFollowingNewFollowingExpansionSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.NewFollowingNewFollowingExpansionSourceWeight, - 1d, - 0d, - 1000d) - // Activity based - case object NewFollowingSimilarUserSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.NewFollowingSimilarUserSourceWeight, - 1d, - 0d, - 1000d) - case object RecentEngagementSimilarUserSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.RecentEngagementSimilarUserSourceWeight, - 1d, - 0d, - 1000d) - case object RepeatedProfileVisitsSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.RepeatedProfileVisitsSourceWeight, - 1d, - 0d, - 1000d) - case object RealGraphOonSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.RealGraphOonSourceWeight, - 1d, - 0d, - 1000d) - // Geo based - case object PopCountrySourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.PopCountrySourceWeight, - 1d, - 0d, - 1000d) - case object PopGeohashSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.PopGeohashSourceWeight, - 1d, - 0d, - 1000d) - case object PopCountryBackfillSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.PopCountryBackfillSourceWeight, - 1d, - 0d, - 1000d) - case object PPMILocaleFollowSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.PPMILocaleFollowSourceWeight, - 1d, - 0d, - 1000d) - case object TopOrganicFollowsAccountsSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.TopOrganicFollowsAccountsSourceWeight, - 1d, - 0d, - 1000d) - case object CrowdSearchAccountSourceWeight - extends FSBoundedParam[Double]( - ContentRecommenderFlowFeatureSwitchKeys.CrowdSearchAccountSourceWeight, - 1d, - 0d, - 1000d) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowFSConfig.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowFSConfig.scala deleted file mode 100644 index a24032c84..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowFSConfig.scala +++ /dev/null @@ -1,60 +0,0 @@ -package com.twitter.follow_recommendations.flows.content_recommender_flow - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.Param - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ContentRecommenderFlowFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = - Seq( - ContentRecommenderParams.IncludeActivityBasedCandidateSource, - ContentRecommenderParams.IncludeSocialBasedCandidateSource, - ContentRecommenderParams.IncludeGeoBasedCandidateSource, - ContentRecommenderParams.IncludeHomeTimelineTweetRecsCandidateSource, - ContentRecommenderParams.IncludeSocialProofEnforcedCandidateSource, - ContentRecommenderParams.EnableRecentFollowingPredicate, - ContentRecommenderParams.EnableGizmoduckPredicate, - ContentRecommenderParams.EnableInactivePredicate, - ContentRecommenderParams.EnableInvalidTargetCandidateRelationshipPredicate, - ContentRecommenderParams.IncludeNewFollowingNewFollowingExpansionCandidateSource, - ContentRecommenderParams.IncludeMoreGeoBasedCandidateSource, - ContentRecommenderParams.TargetEligibility, - ContentRecommenderParams.GetFollowersFromSgs, - ContentRecommenderParams.EnableInvalidRelationshipPredicate, - ) - - override val intFSParams: Seq[FSBoundedParam[Int]] = - Seq( - ContentRecommenderParams.ResultSizeParam, - ContentRecommenderParams.BatchSizeParam, - ContentRecommenderParams.FetchCandidateSourceBudgetInMillisecond, - ContentRecommenderParams.RecentFollowingPredicateBudgetInMillisecond, - ) - - override val doubleFSParams: Seq[FSBoundedParam[Double]] = - Seq( - ContentRecommenderFlowCandidateSourceWeightsParams.ForwardPhoneBookSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.ForwardEmailBookSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.ReversePhoneBookSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.ReverseEmailBookSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.OfflineStrongTiePredictionSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.TriangularLoopsSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.UserUserGraphSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.NewFollowingNewFollowingExpansionSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.NewFollowingSimilarUserSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.RecentEngagementSimilarUserSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.RepeatedProfileVisitsSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.RealGraphOonSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.PopCountrySourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.PopGeohashSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.PopCountryBackfillSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.PPMILocaleFollowSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.TopOrganicFollowsAccountsSourceWeight, - ContentRecommenderFlowCandidateSourceWeightsParams.CrowdSearchAccountSourceWeight, - ) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowFeatureSwitchKeys.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowFeatureSwitchKeys.scala deleted file mode 100644 index ff51dc9f6..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderFlowFeatureSwitchKeys.scala +++ /dev/null @@ -1,70 +0,0 @@ -package com.twitter.follow_recommendations.flows.content_recommender_flow - -object ContentRecommenderFlowFeatureSwitchKeys { - val TargetUserEligible = "content_recommender_flow_target_eligible" - val ResultSize = "content_recommender_flow_result_size" - val BatchSize = "content_recommender_flow_batch_size" - val RecentFollowingPredicateBudgetInMillisecond = - "content_recommender_flow_recent_following_predicate_budget_in_ms" - val CandidateGenerationBudgetInMillisecond = - "content_recommender_flow_candidate_generation_budget_in_ms" - val EnableRecentFollowingPredicate = "content_recommender_flow_enable_recent_following_predicate" - val EnableGizmoduckPredicate = "content_recommender_flow_enable_gizmoduck_predicate" - val EnableInactivePredicate = "content_recommender_flow_enable_inactive_predicate" - val EnableInvalidTargetCandidateRelationshipPredicate = - "content_recommender_flow_enable_invalid_target_candidate_relationship_predicate" - val IncludeActivityBasedCandidateSource = - "content_recommender_flow_include_activity_based_candidate_source" - val IncludeSocialBasedCandidateSource = - "content_recommender_flow_include_social_based_candidate_source" - val IncludeGeoBasedCandidateSource = - "content_recommender_flow_include_geo_based_candidate_source" - val IncludeHomeTimelineTweetRecsCandidateSource = - "content_recommender_flow_include_home_timeline_tweet_recs_candidate_source" - val IncludeSocialProofEnforcedCandidateSource = - "content_recommender_flow_include_social_proof_enforced_candidate_source" - val IncludeNewFollowingNewFollowingExpansionCandidateSource = - "content_recommender_flow_include_new_following_new_following_expansion_candidate_source" - val IncludeMoreGeoBasedCandidateSource = - "content_recommender_flow_include_more_geo_based_candidate_source" - val GetFollowersFromSgs = "content_recommender_flow_get_followers_from_sgs" - val EnableInvalidRelationshipPredicate = - "content_recommender_flow_enable_invalid_relationship_predicate" - - // Candidate source weight param keys - // Social based - val ForwardPhoneBookSourceWeight = - "content_recommender_flow_candidate_source_weight_forward_phone_book" - val ForwardEmailBookSourceWeight = - "content_recommender_flow_candidate_source_weight_forward_email_book" - val ReversePhoneBookSourceWeight = - "content_recommender_flow_candidate_source_weight_reverse_phone_book" - val ReverseEmailBookSourceWeight = - "content_recommender_flow_candidate_source_weight_reverse_email_book" - val OfflineStrongTiePredictionSourceWeight = - "content_recommender_flow_candidate_source_weight_offline_stp" - val TriangularLoopsSourceWeight = - "content_recommender_flow_candidate_source_weight_triangular_loops" - val UserUserGraphSourceWeight = "content_recommender_flow_candidate_source_weight_user_user_graph" - val NewFollowingNewFollowingExpansionSourceWeight = - "content_recommender_flow_candidate_source_weight_new_following_new_following_expansion" - // Activity based - val NewFollowingSimilarUserSourceWeight = - "content_recommender_flow_candidate_source_weight_new_following_similar_user" - val RecentEngagementSimilarUserSourceWeight = - "content_recommender_flow_candidate_source_weight_recent_engagement_similar_user" - val RepeatedProfileVisitsSourceWeight = - "content_recommender_flow_candidate_source_weight_repeated_profile_visits" - val RealGraphOonSourceWeight = "content_recommender_flow_candidate_source_weight_real_graph_oon" - // Geo based - val PopCountrySourceWeight = "content_recommender_flow_candidate_source_weight_pop_country" - val PopGeohashSourceWeight = "content_recommender_flow_candidate_source_weight_pop_geohash" - val PopCountryBackfillSourceWeight = - "content_recommender_flow_candidate_source_weight_pop_country_backfill" - val PPMILocaleFollowSourceWeight = - "content_recommender_flow_candidate_source_weight_ppmi_locale_follow" - val TopOrganicFollowsAccountsSourceWeight = - "content_recommender_flow_candidate_source_weight_top_organic_follow_account" - val CrowdSearchAccountSourceWeight = - "content_recommender_flow_candidate_source_weight_crowd_search_account" -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderParams.scala deleted file mode 100644 index 6b43325af..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderParams.scala +++ /dev/null @@ -1,85 +0,0 @@ -package com.twitter.follow_recommendations.flows.content_recommender_flow - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.Param - -abstract class ContentRecommenderParams[A](default: A) extends Param[A](default) { - override val statName: String = "content_recommender/" + this.getClass.getSimpleName -} - -object ContentRecommenderParams { - - case object TargetEligibility - extends FSParam[Boolean](ContentRecommenderFlowFeatureSwitchKeys.TargetUserEligible, true) - - case object ResultSizeParam - extends FSBoundedParam[Int](ContentRecommenderFlowFeatureSwitchKeys.ResultSize, 15, 1, 500) - case object BatchSizeParam - extends FSBoundedParam[Int](ContentRecommenderFlowFeatureSwitchKeys.BatchSize, 15, 1, 500) - case object RecentFollowingPredicateBudgetInMillisecond - extends FSBoundedParam[Int]( - ContentRecommenderFlowFeatureSwitchKeys.RecentFollowingPredicateBudgetInMillisecond, - 8, - 1, - 50) - case object FetchCandidateSourceBudgetInMillisecond - extends FSBoundedParam[Int]( - ContentRecommenderFlowFeatureSwitchKeys.CandidateGenerationBudgetInMillisecond, - 60, - 1, - 80) - case object EnableRecentFollowingPredicate - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.EnableRecentFollowingPredicate, - true) - case object EnableGizmoduckPredicate - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.EnableGizmoduckPredicate, - false) - case object EnableInactivePredicate - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.EnableInactivePredicate, - false) - case object EnableInvalidTargetCandidateRelationshipPredicate - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.EnableInvalidTargetCandidateRelationshipPredicate, - false) - case object IncludeActivityBasedCandidateSource - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.IncludeActivityBasedCandidateSource, - true) - case object IncludeSocialBasedCandidateSource - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.IncludeSocialBasedCandidateSource, - true) - case object IncludeGeoBasedCandidateSource - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.IncludeGeoBasedCandidateSource, - true) - case object IncludeHomeTimelineTweetRecsCandidateSource - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.IncludeHomeTimelineTweetRecsCandidateSource, - false) - case object IncludeSocialProofEnforcedCandidateSource - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.IncludeSocialProofEnforcedCandidateSource, - false) - case object IncludeNewFollowingNewFollowingExpansionCandidateSource - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.IncludeNewFollowingNewFollowingExpansionCandidateSource, - false) - - case object IncludeMoreGeoBasedCandidateSource - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.IncludeMoreGeoBasedCandidateSource, - false) - - case object GetFollowersFromSgs - extends FSParam[Boolean](ContentRecommenderFlowFeatureSwitchKeys.GetFollowersFromSgs, false) - - case object EnableInvalidRelationshipPredicate - extends FSParam[Boolean]( - ContentRecommenderFlowFeatureSwitchKeys.EnableInvalidRelationshipPredicate, - false) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderRequest.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderRequest.scala deleted file mode 100644 index 5952314e5..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderRequest.scala +++ /dev/null @@ -1,45 +0,0 @@ -package com.twitter.follow_recommendations.flows.content_recommender_flow - -import com.twitter.core_workflows.user_model.thriftscala.UserState -import com.twitter.follow_recommendations.common.models.DebugOptions -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.common.models.GeohashAndCountryCode -import com.twitter.follow_recommendations.common.models.HasDebugOptions -import com.twitter.follow_recommendations.common.models.HasDisplayLocation -import com.twitter.follow_recommendations.common.models.HasExcludedUserIds -import com.twitter.follow_recommendations.common.models.HasGeohashAndCountryCode -import com.twitter.follow_recommendations.common.models.HasInvalidRelationshipUserIds -import com.twitter.follow_recommendations.common.models.HasRecentFollowedByUserIds -import com.twitter.follow_recommendations.common.models.HasRecentFollowedUserIds -import com.twitter.follow_recommendations.common.models.HasUserState -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Params - -case class ContentRecommenderRequest( - override val params: Params, - override val clientContext: ClientContext, - inputExcludeUserIds: Seq[Long], - override val recentFollowedUserIds: Option[Seq[Long]], - override val recentFollowedByUserIds: Option[Seq[Long]], - override val invalidRelationshipUserIds: Option[Set[Long]], - override val displayLocation: DisplayLocation, - maxResults: Option[Int] = None, - override val debugOptions: Option[DebugOptions] = None, - override val geohashAndCountryCode: Option[GeohashAndCountryCode] = None, - override val userState: Option[UserState] = None) - extends HasParams - with HasClientContext - with HasDisplayLocation - with HasDebugOptions - with HasRecentFollowedUserIds - with HasRecentFollowedByUserIds - with HasInvalidRelationshipUserIds - with HasExcludedUserIds - with HasUserState - with HasGeohashAndCountryCode { - override val excludedUserIds: Seq[Long] = { - inputExcludeUserIds ++ clientContext.userId.toSeq - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderRequestBuilder.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderRequestBuilder.scala deleted file mode 100644 index 769f9ce51..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow/ContentRecommenderRequestBuilder.scala +++ /dev/null @@ -1,121 +0,0 @@ -package com.twitter.follow_recommendations.flows.content_recommender_flow - -import com.twitter.conversions.DurationOps._ -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.clients.geoduck.UserLocationFetcher -import com.twitter.follow_recommendations.common.clients.socialgraph.SocialGraphClient -import com.twitter.follow_recommendations.common.clients.user_state.UserStateClient -import com.twitter.follow_recommendations.common.utils.RescueWithStatsUtils.rescueOptionalWithStats -import com.twitter.follow_recommendations.common.utils.RescueWithStatsUtils.rescueWithStats -import com.twitter.follow_recommendations.common.utils.RescueWithStatsUtils.rescueWithStatsWithin -import com.twitter.follow_recommendations.products.common.ProductRequest -import com.twitter.stitch.Stitch - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ContentRecommenderRequestBuilder @Inject() ( - socialGraph: SocialGraphClient, - userLocationFetcher: UserLocationFetcher, - userStateClient: UserStateClient, - statsReceiver: StatsReceiver) { - - val stats: StatsReceiver = statsReceiver.scope("content_recommender_request_builder") - val invalidRelationshipUsersStats: StatsReceiver = stats.scope("invalidRelationshipUserIds") - private val invalidRelationshipUsersMaxSizeCounter = - invalidRelationshipUsersStats.counter("maxSize") - private val invalidRelationshipUsersNotMaxSizeCounter = - invalidRelationshipUsersStats.counter("notMaxSize") - - def build(req: ProductRequest): Stitch[ContentRecommenderRequest] = { - val userStateStitch = Stitch - .collect(req.recommendationRequest.clientContext.userId.map(userId => - userStateClient.getUserState(userId))).map(_.flatten) - val recentFollowedUserIdsStitch = - Stitch - .collect(req.recommendationRequest.clientContext.userId.map { userId => - rescueWithStatsWithin( - socialGraph.getRecentFollowedUserIds(userId), - stats, - "recentFollowedUserIds", - req - .params( - ContentRecommenderParams.RecentFollowingPredicateBudgetInMillisecond).millisecond - ) - }) - val recentFollowedByUserIdsStitch = - if (req.params(ContentRecommenderParams.GetFollowersFromSgs)) { - Stitch - .collect( - req.recommendationRequest.clientContext.userId.map(userId => - rescueWithStatsWithin( - socialGraph.getRecentFollowedByUserIdsFromCachedColumn(userId), - stats, - "recentFollowedByUserIds", - req - .params(ContentRecommenderParams.RecentFollowingPredicateBudgetInMillisecond) - .millisecond - ))) - } else Stitch.None - val invalidRelationshipUserIdsStitch: Stitch[Option[Seq[Long]]] = - if (req.params(ContentRecommenderParams.EnableInvalidRelationshipPredicate)) { - Stitch - .collect( - req.recommendationRequest.clientContext.userId.map { userId => - rescueWithStats( - socialGraph - .getInvalidRelationshipUserIdsFromCachedColumn(userId) - .onSuccess(ids => - if (ids.size >= SocialGraphClient.MaxNumInvalidRelationship) { - invalidRelationshipUsersMaxSizeCounter.incr() - } else { - invalidRelationshipUsersNotMaxSizeCounter.incr() - }), - stats, - "invalidRelationshipUserIds" - ) - } - ) - } else { - Stitch.None - } - val locationStitch = - rescueOptionalWithStats( - userLocationFetcher.getGeohashAndCountryCode( - req.recommendationRequest.clientContext.userId, - req.recommendationRequest.clientContext.ipAddress - ), - stats, - "userLocation" - ) - Stitch - .join( - recentFollowedUserIdsStitch, - recentFollowedByUserIdsStitch, - invalidRelationshipUserIdsStitch, - locationStitch, - userStateStitch) - .map { - case ( - recentFollowedUserIds, - recentFollowedByUserIds, - invalidRelationshipUserIds, - location, - userState) => - ContentRecommenderRequest( - req.params, - req.recommendationRequest.clientContext, - req.recommendationRequest.excludedIds.getOrElse(Nil), - recentFollowedUserIds, - recentFollowedByUserIds, - invalidRelationshipUserIds.map(_.toSet), - req.recommendationRequest.displayLocation, - req.recommendationRequest.maxResults, - req.recommendationRequest.debugParams.flatMap(_.debugOptions), - location, - userState - ) - } - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/BUILD deleted file mode 100644 index 9129e17b8..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/BUILD +++ /dev/null @@ -1,58 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/salsa", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/top_organic_follows_accounts", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/user_user_graph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/deepbirdv2", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/geoduck", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/interests_service", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/user_state", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/dismiss", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/gizmoduck", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/health", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/user_activity", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/fatigue_ranker", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/first_n_ranker", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/interleave_ranker", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/weighted_candidate_source_ranker", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/dedup", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/ranker_id", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/tracking_token", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/weighted_sampling", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/candidates", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/logging", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCandidateSourceRegistry.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCandidateSourceRegistry.scala deleted file mode 100644 index ed15d566c..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCandidateSourceRegistry.scala +++ /dev/null @@ -1,103 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.CandidateSourceRegistry -import com.twitter.follow_recommendations.common.base.EnrichedCandidateSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ForwardEmailBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ForwardPhoneBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ReverseEmailBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ReversePhoneBookSource -import com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts.CrowdSearchAccountsSource -import com.twitter.follow_recommendations.common.candidate_sources.top_organic_follows_accounts.TopOrganicFollowsAccountsSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopCountrySource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopCountryBackFillSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeohashQualityFollowSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeohashSource -import com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow.PPMILocaleFollowSource -import com.twitter.follow_recommendations.common.candidate_sources.real_graph.RealGraphOonV2Source -import com.twitter.follow_recommendations.common.candidate_sources.recent_engagement.RecentEngagementNonDirectFollowSource -import com.twitter.follow_recommendations.common.candidate_sources.recent_engagement.RepeatedProfileVisitsSource -import com.twitter.follow_recommendations.common.candidate_sources.salsa.RecentEngagementDirectFollowSalsaExpansionSource -import com.twitter.follow_recommendations.common.candidate_sources.sims.LinearRegressionFollow2vecNearestNeighborsStore -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentEngagementSimilarUsersSource -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentFollowingSimilarUsersSource -import com.twitter.follow_recommendations.common.candidate_sources.stp.OnlineSTPSourceScorer -import com.twitter.follow_recommendations.common.candidate_sources.stp.OfflineStrongTiePredictionSource -import com.twitter.follow_recommendations.common.candidate_sources.triangular_loops.TriangularLoopsSource -import com.twitter.follow_recommendations.common.candidate_sources.user_user_graph.UserUserGraphCandidateSource -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PostNuxMlCandidateSourceRegistry @Inject() ( - crowdSearchAccountsCandidateSource: CrowdSearchAccountsSource, - topOrganicFollowsAccountsSource: TopOrganicFollowsAccountsSource, - linearRegressionfollow2vecNearestNeighborsStore: LinearRegressionFollow2vecNearestNeighborsStore, - forwardEmailBookSource: ForwardEmailBookSource, - forwardPhoneBookSource: ForwardPhoneBookSource, - offlineStrongTiePredictionSource: OfflineStrongTiePredictionSource, - onlineSTPSource: OnlineSTPSourceScorer, - popCountrySource: PopCountrySource, - popCountryBackFillSource: PopCountryBackFillSource, - popGeohashSource: PopGeohashSource, - recentEngagementDirectFollowSimilarUsersSource: RecentEngagementSimilarUsersSource, - recentEngagementNonDirectFollowSource: RecentEngagementNonDirectFollowSource, - recentEngagementDirectFollowSalsaExpansionSource: RecentEngagementDirectFollowSalsaExpansionSource, - recentFollowingSimilarUsersSource: RecentFollowingSimilarUsersSource, - realGraphOonV2Source: RealGraphOonV2Source, - repeatedProfileVisitSource: RepeatedProfileVisitsSource, - reverseEmailBookSource: ReverseEmailBookSource, - reversePhoneBookSource: ReversePhoneBookSource, - triangularLoopsSource: TriangularLoopsSource, - userUserGraphCandidateSource: UserUserGraphCandidateSource, - ppmiLocaleFollowSource: PPMILocaleFollowSource, - popGeohashQualityFollowSource: PopGeohashQualityFollowSource, - baseStatsReceiver: StatsReceiver, -) extends CandidateSourceRegistry[PostNuxMlRequest, CandidateUser] { - import EnrichedCandidateSource._ - - override val statsReceiver = baseStatsReceiver - .scope("post_nux_ml_flow", "candidate_sources") - - // sources primarily based on social graph signals - private[this] val socialSources = Seq( - linearRegressionfollow2vecNearestNeighborsStore.mapKeys[PostNuxMlRequest]( - _.getOptionalUserId.toSeq), - forwardEmailBookSource, - forwardPhoneBookSource, - offlineStrongTiePredictionSource, - onlineSTPSource, - reverseEmailBookSource, - reversePhoneBookSource, - triangularLoopsSource, - ) - - // sources primarily based on geo signals - private[this] val geoSources = Seq( - popCountrySource, - popCountryBackFillSource, - popGeohashSource, - popGeohashQualityFollowSource, - topOrganicFollowsAccountsSource, - crowdSearchAccountsCandidateSource, - ppmiLocaleFollowSource, - ) - - // sources primarily based on recent activity signals - private[this] val activitySources = Seq( - repeatedProfileVisitSource, - recentEngagementDirectFollowSalsaExpansionSource.mapKeys[PostNuxMlRequest]( - _.getOptionalUserId.toSeq), - recentEngagementDirectFollowSimilarUsersSource, - recentEngagementNonDirectFollowSource.mapKeys[PostNuxMlRequest](_.getOptionalUserId.toSeq), - recentFollowingSimilarUsersSource, - realGraphOonV2Source, - userUserGraphCandidateSource, - ) - - override val sources: Set[CandidateSource[PostNuxMlRequest, CandidateUser]] = ( - geoSources ++ socialSources ++ activitySources - ).toSet -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCandidateSourceWeightParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCandidateSourceWeightParams.scala deleted file mode 100644 index 9492747a4..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCandidateSourceWeightParams.scala +++ /dev/null @@ -1,177 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.Param - -abstract class PostNuxMlCandidateSourceWeightParams[A](default: A) extends Param[A](default) { - override val statName: String = "post_nux_ml/" + this.getClass.getSimpleName -} - -object PostNuxMlCandidateSourceWeightParams { - - case object CandidateWeightCrowdSearch - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightCrowdSearch, - 1.0, - 0.0, - 1000.0 - ) - - case object CandidateWeightTopOrganicFollow - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightTopOrganicFollow, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightPPMILocaleFollow - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightPPMILocaleFollow, - 1.0, - 0.0, - 1000.0 - ) - - case object CandidateWeightForwardEmailBook - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightForwardEmailBook, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightForwardPhoneBook - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightForwardPhoneBook, - 1.0, - 0.0, - 1000.0 - ) - - case object CandidateWeightOfflineStrongTiePrediction - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightOfflineStrongTiePrediction, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightOnlineStp - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightOnlineStp, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightPopCountry - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightPopCountry, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightPopGeohash - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightPopGeohash, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightPopGeohashQualityFollow - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightPopGeohashQualityFollow, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightPopGeoBackfill - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightPopGeoBackfill, - 1, - 0.0, - 1000.0 - ) - case object CandidateWeightRecentFollowingSimilarUsers - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightRecentFollowingSimilarUsers, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightRecentEngagementDirectFollowSalsaExpansion - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightRecentEngagementDirectFollowSalsaExpansion, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightRecentEngagementNonDirectFollow - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightRecentEngagementNonDirectFollow, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightRecentEngagementSimilarUsers - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightRecentEngagementSimilarUsers, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightRepeatedProfileVisits - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightRepeatedProfileVisits, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightFollow2vecNearestNeighbors - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightFollow2vecNearestNeighbors, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightReverseEmailBook - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightReverseEmailBook, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightReversePhoneBook - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightReversePhoneBook, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightTriangularLoops - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightTriangularLoops, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightTwoHopRandomWalk - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightTwoHopRandomWalk, - 1.0, - 0.0, - 1000.0 - ) - case object CandidateWeightUserUserGraph - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightUserUserGraph, - 1.0, - 0.0, - 1000.0 - ) - - case object CandidateWeightRealGraphOonV2 - extends FSBoundedParam[Double]( - PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.CandidateWeightRealGraphOonV2, - 1.0, - 0.0, - 2000.0 - ) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCombinedRankerBuilder.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCombinedRankerBuilder.scala deleted file mode 100644 index 14e982a41..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlCombinedRankerBuilder.scala +++ /dev/null @@ -1,193 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -import com.google.inject.Inject -import com.google.inject.Singleton -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.IdentityRanker -import com.twitter.follow_recommendations.common.base.IdentityTransform -import com.twitter.follow_recommendations.common.base.Ranker -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.models._ -import com.twitter.follow_recommendations.common.rankers.common.RankerId -import com.twitter.follow_recommendations.common.rankers.fatigue_ranker.ImpressionBasedFatigueRanker -import com.twitter.follow_recommendations.common.rankers.first_n_ranker.FirstNRanker -import com.twitter.follow_recommendations.common.rankers.first_n_ranker.FirstNRankerParams -import com.twitter.follow_recommendations.common.rankers.interleave_ranker.InterleaveRanker -import com.twitter.follow_recommendations.common.rankers.ml_ranker.ranking.HydrateFeaturesTransform -import com.twitter.follow_recommendations.common.rankers.ml_ranker.ranking.MlRanker -import com.twitter.follow_recommendations.common.rankers.ml_ranker.ranking.MlRankerParams -import com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker.WeightedCandidateSourceRanker -import com.twitter.follow_recommendations.configapi.candidates.HydrateCandidateParamsTransform -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.timelines.configapi.HasParams - -/** - * Used to build the combined ranker comprising 4 stages of ranking: - * - weighted sampler - * - truncating to the top N merged results for ranking - * - ML ranker - * - Interleaving ranker for producer-side experiments - * - impression-based fatigueing - */ -@Singleton -class PostNuxMlCombinedRankerBuilder[ - T <: HasParams with HasSimilarToContext with HasClientContext with HasExcludedUserIds with HasDisplayLocation with HasDebugOptions with HasPreFetchedFeature with HasDismissedUserIds with HasQualityFactor] @Inject() ( - firstNRanker: FirstNRanker[T], - hydrateFeaturesTransform: HydrateFeaturesTransform[T], - hydrateCandidateParamsTransform: HydrateCandidateParamsTransform[T], - mlRanker: MlRanker[T], - statsReceiver: StatsReceiver) { - private[this] val stats: StatsReceiver = statsReceiver.scope("post_nux_ml_ranker") - - // we construct each ranker independently and chain them together - def build( - request: T, - candidateSourceWeights: Map[CandidateSourceIdentifier, Double] - ): Ranker[T, CandidateUser] = { - val displayLocationStats = stats.scope(request.displayLocation.toString) - val weightedRankerStats: StatsReceiver = - displayLocationStats.scope("weighted_candidate_source_ranker") - val firstNRankerStats: StatsReceiver = - displayLocationStats.scope("first_n_ranker") - val hydrateCandidateParamsStats = - displayLocationStats.scope("hydrate_candidate_params") - val fatigueRankerStats = displayLocationStats.scope("fatigue_ranker") - val interleaveRankerStats = - displayLocationStats.scope("interleave_ranker") - val allRankersStats = displayLocationStats.scope("all_rankers") - - // Checking if the heavy-ranker is an experimental model. - // If it is, InterleaveRanker and candidate parameter hydration are disabled. - // *NOTE* that consumer-side experiments should at any time take a small % of traffic, less - // than 20% for instance, to leave enough room for producer experiments. Increasing bucket - // size for producer experiments lead to other issues and is not a viable option for faster - // experiments. - val requestRankerId = request.params(MlRankerParams.RequestScorerIdParam) - if (requestRankerId != RankerId.PostNuxProdRanker) { - hydrateCandidateParamsStats.counter(s"disabled_by_${requestRankerId.toString}").incr() - interleaveRankerStats.counter(s"disabled_by_${requestRankerId.toString}").incr() - } - - // weighted ranker that samples from the candidate sources - val weightedRanker = WeightedCandidateSourceRanker - .build[T]( - candidateSourceWeights, - request.params(PostNuxMlParams.CandidateShuffler).shuffle(request.getRandomizationSeed), - randomSeed = request.getRandomizationSeed - ).observe(weightedRankerStats) - - // ranker that takes the first n results (ie truncates output) while merging duplicates - val firstNRankerObs = firstNRanker.observe(firstNRankerStats) - // either ML ranker that uses deepbirdv2 to score or no ranking - val mainRanker: Ranker[T, CandidateUser] = - buildMainRanker(request, requestRankerId == RankerId.PostNuxProdRanker, displayLocationStats) - // fatigue ranker that uses wtf impressions to fatigue - val fatigueRanker = buildFatigueRanker(request, fatigueRankerStats).observe(fatigueRankerStats) - - // interleaveRanker combines rankings from several rankers and enforces candidates' ranks in - // experiment buckets according to their assigned ranker model. - val interleaveRanker = - buildInterleaveRanker( - request, - requestRankerId == RankerId.PostNuxProdRanker, - interleaveRankerStats) - .observe(interleaveRankerStats) - - weightedRanker - .andThen(firstNRankerObs) - .andThen(mainRanker) - .andThen(fatigueRanker) - .andThen(interleaveRanker) - .observe(allRankersStats) - } - - def buildMainRanker( - request: T, - isMainRankerPostNuxProd: Boolean, - displayLocationStats: StatsReceiver - ): Ranker[T, CandidateUser] = { - - // note that we may be disabling heavy ranker for users not bucketed - // (due to empty results from the new candidate source) - // need a better solution in the future - val mlRankerStats = displayLocationStats.scope("ml_ranker") - val noMlRankerStats = displayLocationStats.scope("no_ml_ranker") - val hydrateFeaturesStats = - displayLocationStats.scope("hydrate_features") - val hydrateCandidateParamsStats = - displayLocationStats.scope("hydrate_candidate_params") - val notHydrateCandidateParamsStats = - displayLocationStats.scope("not_hydrate_candidate_params") - val rankerStats = displayLocationStats.scope("ranker") - val mlRankerDisabledByExperimentsCounter = - mlRankerStats.counter("disabled_by_experiments") - val mlRankerDisabledByQualityFactorCounter = - mlRankerStats.counter("disabled_by_quality_factor") - - val disabledByQualityFactor = request.qualityFactor - .exists(_ <= request.params(PostNuxMlParams.TurnoffMLScorerQFThreshold)) - - if (disabledByQualityFactor) - mlRankerDisabledByQualityFactorCounter.incr() - - if (request.params(PostNuxMlParams.UseMlRanker) && !disabledByQualityFactor) { - - val hydrateFeatures = hydrateFeaturesTransform - .observe(hydrateFeaturesStats) - - val optionalHydratedParamsTransform: Transform[T, CandidateUser] = { - // We disable candidate parameter hydration for experimental heavy-ranker models. - if (isMainRankerPostNuxProd && - request.params(PostNuxMlParams.EnableCandidateParamHydration)) { - hydrateCandidateParamsTransform - .observe(hydrateCandidateParamsStats) - } else { - new IdentityTransform[T, CandidateUser]() - .observe(notHydrateCandidateParamsStats) - } - } - val candidateSize = request.params(FirstNRankerParams.CandidatesToRank) - Ranker - .chain( - hydrateFeatures.andThen(optionalHydratedParamsTransform), - mlRanker.observe(mlRankerStats), - ) - .within( - request.params(PostNuxMlParams.MlRankerBudget), - rankerStats.scope(s"n$candidateSize")) - } else { - new IdentityRanker[T, CandidateUser].observe(noMlRankerStats) - } - } - - def buildInterleaveRanker( - request: T, - isMainRankerPostNuxProd: Boolean, - interleaveRankerStats: StatsReceiver - ): Ranker[T, CandidateUser] = { - // InterleaveRanker is enabled only for display locations powered by the PostNux heavy-ranker. - if (request.params(PostNuxMlParams.EnableInterleaveRanker) && - // InterleaveRanker is disabled for requests with experimental heavy-rankers. - isMainRankerPostNuxProd) { - new InterleaveRanker[T](interleaveRankerStats) - } else { - new IdentityRanker[T, CandidateUser]() - } - } - - def buildFatigueRanker( - request: T, - fatigueRankerStats: StatsReceiver - ): Ranker[T, CandidateUser] = { - if (request.params(PostNuxMlParams.EnableFatigueRanker)) { - ImpressionBasedFatigueRanker - .build[T]( - fatigueRankerStats - ).within(request.params(PostNuxMlParams.FatigueRankerBudget), fatigueRankerStats) - } else { - new IdentityRanker[T, CandidateUser]() - } - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlow.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlow.scala deleted file mode 100644 index 092f07100..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlow.scala +++ /dev/null @@ -1,304 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -import com.twitter.conversions.DurationOps._ -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.EnrichedCandidateSource._ -import com.twitter.follow_recommendations.common.base._ -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.FilterReason -import com.twitter.follow_recommendations.common.predicates.dismiss.DismissedCandidatePredicate -import com.twitter.follow_recommendations.common.predicates.gizmoduck.GizmoduckPredicate -import com.twitter.follow_recommendations.common.transforms.ranker_id.RandomRankerIdTransform -import com.twitter.follow_recommendations.common.predicates.sgs.InvalidTargetCandidateRelationshipTypesPredicate -import com.twitter.follow_recommendations.common.predicates.sgs.RecentFollowingPredicate -import com.twitter.follow_recommendations.common.predicates.CandidateParamPredicate -import com.twitter.follow_recommendations.common.predicates.CandidateSourceParamPredicate -import com.twitter.follow_recommendations.common.predicates.CuratedCompetitorListPredicate -import com.twitter.follow_recommendations.common.predicates.ExcludedUserIdPredicate -import com.twitter.follow_recommendations.common.predicates.InactivePredicate -import com.twitter.follow_recommendations.common.predicates.PreviouslyRecommendedUserIdsPredicate -import com.twitter.follow_recommendations.common.predicates.user_activity.NonNearZeroUserActivityPredicate -import com.twitter.follow_recommendations.common.transforms.dedup.DedupTransform -import com.twitter.follow_recommendations.common.transforms.modify_social_proof.ModifySocialProofTransform -import com.twitter.follow_recommendations.common.transforms.tracking_token.TrackingTokenTransform -import com.twitter.follow_recommendations.common.transforms.weighted_sampling.SamplingTransform -import com.twitter.follow_recommendations.configapi.candidates.CandidateUserParamsFactory -import com.twitter.follow_recommendations.configapi.params.GlobalParams -import com.twitter.follow_recommendations.configapi.params.GlobalParams.EnableGFSSocialProofTransform -import com.twitter.follow_recommendations.utils.CandidateSourceHoldbackUtil -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.timelines.configapi.Params -import com.twitter.util.Duration - -import javax.inject.Inject -import javax.inject.Singleton -import com.twitter.follow_recommendations.common.clients.socialgraph.SocialGraphClient -import com.twitter.follow_recommendations.common.predicates.hss.HssPredicate -import com.twitter.follow_recommendations.common.predicates.sgs.InvalidRelationshipPredicate -import com.twitter.follow_recommendations.common.transforms.modify_social_proof.RemoveAccountProofTransform -import com.twitter.follow_recommendations.logging.FrsLogger -import com.twitter.follow_recommendations.models.RecommendationFlowData -import com.twitter.follow_recommendations.utils.RecommendationFlowBaseSideEffectsUtil -import com.twitter.product_mixer.core.model.common.identifier.RecommendationPipelineIdentifier -import com.twitter.product_mixer.core.quality_factor.BoundsWithDefault -import com.twitter.product_mixer.core.quality_factor.LinearLatencyQualityFactor -import com.twitter.product_mixer.core.quality_factor.LinearLatencyQualityFactorConfig -import com.twitter.product_mixer.core.quality_factor.LinearLatencyQualityFactorObserver -import com.twitter.product_mixer.core.quality_factor.QualityFactorObserver -import com.twitter.stitch.Stitch - -/** - * We use this flow for all post-nux display locations that would use a machine-learning-based-ranker - * eg HTL, Sidebar, etc - * Note that the RankedPostNuxFlow is used primarily for scribing/data collection, and doesn't - * incorporate all of the other components in a flow (candidate source generation, predicates etc) - */ -@Singleton -class PostNuxMlFlow @Inject() ( - postNuxMlCandidateSourceRegistry: PostNuxMlCandidateSourceRegistry, - postNuxMlCombinedRankerBuilder: PostNuxMlCombinedRankerBuilder[PostNuxMlRequest], - curatedCompetitorListPredicate: CuratedCompetitorListPredicate, - gizmoduckPredicate: GizmoduckPredicate, - sgsPredicate: InvalidTargetCandidateRelationshipTypesPredicate, - hssPredicate: HssPredicate, - invalidRelationshipPredicate: InvalidRelationshipPredicate, - recentFollowingPredicate: RecentFollowingPredicate, - nonNearZeroUserActivityPredicate: NonNearZeroUserActivityPredicate, - inactivePredicate: InactivePredicate, - dismissedCandidatePredicate: DismissedCandidatePredicate, - previouslyRecommendedUserIdsPredicate: PreviouslyRecommendedUserIdsPredicate, - modifySocialProofTransform: ModifySocialProofTransform, - removeAccountProofTransform: RemoveAccountProofTransform, - trackingTokenTransform: TrackingTokenTransform, - randomRankerIdTransform: RandomRankerIdTransform, - candidateParamsFactory: CandidateUserParamsFactory[PostNuxMlRequest], - samplingTransform: SamplingTransform, - frsLogger: FrsLogger, - baseStatsReceiver: StatsReceiver) - extends RecommendationFlow[PostNuxMlRequest, CandidateUser] - with RecommendationFlowBaseSideEffectsUtil[PostNuxMlRequest, CandidateUser] - with CandidateSourceHoldbackUtil { - override protected val targetEligibility: Predicate[PostNuxMlRequest] = - new ParamPredicate[PostNuxMlRequest](PostNuxMlParams.TargetEligibility) - - override val statsReceiver: StatsReceiver = baseStatsReceiver.scope("post_nux_ml_flow") - - override val qualityFactorObserver: Option[QualityFactorObserver] = { - val config = LinearLatencyQualityFactorConfig( - qualityFactorBounds = - BoundsWithDefault(minInclusive = 0.1, maxInclusive = 1.0, default = 1.0), - initialDelay = 60.seconds, - targetLatency = 700.milliseconds, - targetLatencyPercentile = 95.0, - delta = 0.001 - ) - val qualityFactor = LinearLatencyQualityFactor(config) - val observer = LinearLatencyQualityFactorObserver(qualityFactor) - statsReceiver.provideGauge("quality_factor")(qualityFactor.currentValue.toFloat) - Some(observer) - } - - override protected def updateTarget(request: PostNuxMlRequest): Stitch[PostNuxMlRequest] = { - Stitch.value( - request.copy(qualityFactor = qualityFactorObserver.map(_.qualityFactor.currentValue)) - ) - } - - private[post_nux_ml] def getCandidateSourceIdentifiers( - params: Params - ): Set[CandidateSourceIdentifier] = { - PostNuxMlFlowCandidateSourceWeights.getWeights(params).keySet - } - - override protected def candidateSources( - request: PostNuxMlRequest - ): Seq[CandidateSource[PostNuxMlRequest, CandidateUser]] = { - val identifiers = getCandidateSourceIdentifiers(request.params) - val selected: Set[CandidateSource[PostNuxMlRequest, CandidateUser]] = - postNuxMlCandidateSourceRegistry.select(identifiers) - val budget: Duration = request.params(PostNuxMlParams.FetchCandidateSourceBudget) - filterCandidateSources( - request, - selected.map(c => c.failOpenWithin(budget, statsReceiver)).toSeq) - } - - override protected val preRankerCandidateFilter: Predicate[(PostNuxMlRequest, CandidateUser)] = { - val stats = statsReceiver.scope("pre_ranker") - - object excludeNearZeroUserPredicate - extends GatedPredicateBase[(PostNuxMlRequest, CandidateUser)]( - nonNearZeroUserActivityPredicate, - stats.scope("exclude_near_zero_predicate") - ) { - override def gate(item: (PostNuxMlRequest, CandidateUser)): Boolean = - item._1.params(PostNuxMlParams.ExcludeNearZeroCandidates) - } - - object invalidRelationshipGatedPredicate - extends GatedPredicateBase[(PostNuxMlRequest, CandidateUser)]( - invalidRelationshipPredicate, - stats.scope("invalid_relationship_predicate") - ) { - override def gate(item: (PostNuxMlRequest, CandidateUser)): Boolean = - item._1.params(PostNuxMlParams.EnableInvalidRelationshipPredicate) - } - - ExcludedUserIdPredicate - .observe(stats.scope("exclude_user_id_predicate")) - .andThen( - recentFollowingPredicate.observe(stats.scope("recent_following_predicate")) - ) - .andThen( - dismissedCandidatePredicate.observe(stats.scope("dismissed_candidate_predicate")) - ) - .andThen( - previouslyRecommendedUserIdsPredicate.observe( - stats.scope("previously_recommended_user_ids_predicate")) - ) - .andThen( - invalidRelationshipGatedPredicate.observe(stats.scope("invalid_relationship_predicate")) - ) - .andThen( - excludeNearZeroUserPredicate.observe(stats.scope("exclude_near_zero_user_state")) - ) - .observe(stats.scope("overall_pre_ranker_candidate_filter")) - } - - override protected def selectRanker( - request: PostNuxMlRequest - ): Ranker[PostNuxMlRequest, CandidateUser] = { - postNuxMlCombinedRankerBuilder.build( - request, - PostNuxMlFlowCandidateSourceWeights.getWeights(request.params)) - } - - override protected val postRankerTransform: Transform[PostNuxMlRequest, CandidateUser] = { - new DedupTransform[PostNuxMlRequest, CandidateUser] - .observe(statsReceiver.scope("dedupping")) - .andThen( - samplingTransform - .gated(PostNuxMlParams.SamplingTransformEnabled) - .observe(statsReceiver.scope("samplingtransform"))) - } - - override protected val validateCandidates: Predicate[(PostNuxMlRequest, CandidateUser)] = { - val stats = statsReceiver.scope("validate_candidates") - val competitorPredicate = - curatedCompetitorListPredicate.map[(PostNuxMlRequest, CandidateUser)](_._2) - - val producerHoldbackPredicate = new CandidateParamPredicate[CandidateUser]( - GlobalParams.KeepUserCandidate, - FilterReason.CandidateSideHoldback - ).map[(PostNuxMlRequest, CandidateUser)] { - case (request, user) => candidateParamsFactory(user, request) - } - val pymkProducerHoldbackPredicate = new CandidateSourceParamPredicate( - GlobalParams.KeepSocialUserCandidate, - FilterReason.CandidateSideHoldback, - CandidateSourceHoldbackUtil.SocialCandidateSourceIds - ).map[(PostNuxMlRequest, CandidateUser)] { - case (request, user) => candidateParamsFactory(user, request) - } - val sgsPredicateStats = stats.scope("sgs_predicate") - object sgsGatedPredicate - extends GatedPredicateBase[(PostNuxMlRequest, CandidateUser)]( - sgsPredicate.observe(sgsPredicateStats), - sgsPredicateStats - ) { - - /** - * When SGS predicate is turned off, only query SGS exists API for (user, candidate, relationship) - * when the user's number of invalid relationships exceeds the threshold during request - * building step. This is to minimize load to SGS and underlying Flock DB. - */ - override def gate(item: (PostNuxMlRequest, CandidateUser)): Boolean = - item._1.params(PostNuxMlParams.EnableSGSPredicate) || - SocialGraphClient.enablePostRankerSgsPredicate( - item._1.invalidRelationshipUserIds.getOrElse(Set.empty).size) - } - - val hssPredicateStats = stats.scope("hss_predicate") - object hssGatedPredicate - extends GatedPredicateBase[(PostNuxMlRequest, CandidateUser)]( - hssPredicate.observe(hssPredicateStats), - hssPredicateStats - ) { - override def gate(item: (PostNuxMlRequest, CandidateUser)): Boolean = - item._1.params(PostNuxMlParams.EnableHssPredicate) - } - - Predicate - .andConcurrently[(PostNuxMlRequest, CandidateUser)]( - Seq( - competitorPredicate.observe(stats.scope("curated_competitor_predicate")), - gizmoduckPredicate.observe(stats.scope("gizmoduck_predicate")), - sgsGatedPredicate, - hssGatedPredicate, - inactivePredicate.observe(stats.scope("inactive_predicate")), - ) - ) - // to avoid dilutions, we need to apply the receiver holdback predicates at the very last step - .andThen(pymkProducerHoldbackPredicate.observe(stats.scope("pymk_receiver_side_holdback"))) - .andThen(producerHoldbackPredicate.observe(stats.scope("receiver_side_holdback"))) - .observe(stats.scope("overall_validate_candidates")) - } - - override protected val transformResults: Transform[PostNuxMlRequest, CandidateUser] = { - modifySocialProofTransform - .gated(EnableGFSSocialProofTransform) - .andThen(trackingTokenTransform) - .andThen(randomRankerIdTransform.gated(PostNuxMlParams.LogRandomRankerId)) - .andThen(removeAccountProofTransform.gated(PostNuxMlParams.EnableRemoveAccountProofTransform)) - } - - override protected def resultsConfig(request: PostNuxMlRequest): RecommendationResultsConfig = { - RecommendationResultsConfig( - request.maxResults.getOrElse(request.params(PostNuxMlParams.ResultSizeParam)), - request.params(PostNuxMlParams.BatchSizeParam) - ) - } - - override def applySideEffects( - target: PostNuxMlRequest, - candidateSources: Seq[CandidateSource[PostNuxMlRequest, CandidateUser]], - candidatesFromCandidateSources: Seq[CandidateUser], - mergedCandidates: Seq[CandidateUser], - filteredCandidates: Seq[CandidateUser], - rankedCandidates: Seq[CandidateUser], - transformedCandidates: Seq[CandidateUser], - truncatedCandidates: Seq[CandidateUser], - results: Seq[CandidateUser] - ): Stitch[Unit] = { - frsLogger.logRecommendationFlowData[PostNuxMlRequest]( - target, - RecommendationFlowData[PostNuxMlRequest]( - target, - PostNuxMlFlow.identifier, - candidateSources, - candidatesFromCandidateSources, - mergedCandidates, - filteredCandidates, - rankedCandidates, - transformedCandidates, - truncatedCandidates, - results - ) - ) - super.applySideEffects( - target, - candidateSources, - candidatesFromCandidateSources, - mergedCandidates, - filteredCandidates, - rankedCandidates, - transformedCandidates, - truncatedCandidates, - results - ) - } -} - -object PostNuxMlFlow { - val identifier = RecommendationPipelineIdentifier("PostNuxMlFlow") -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowCandidateSourceWeights.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowCandidateSourceWeights.scala deleted file mode 100644 index edb447cba..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowCandidateSourceWeights.scala +++ /dev/null @@ -1,68 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ForwardEmailBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ForwardPhoneBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ReverseEmailBookSource -import com.twitter.follow_recommendations.common.candidate_sources.addressbook.ReversePhoneBookSource -import com.twitter.follow_recommendations.common.candidate_sources.crowd_search_accounts.CrowdSearchAccountsSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopCountryBackFillSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopCountrySource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeohashQualityFollowSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeohashSource -import com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow.PPMILocaleFollowSource -import com.twitter.follow_recommendations.common.candidate_sources.real_graph.RealGraphOonV2Source -import com.twitter.follow_recommendations.common.candidate_sources.recent_engagement.RecentEngagementNonDirectFollowSource -import com.twitter.follow_recommendations.common.candidate_sources.recent_engagement.RepeatedProfileVisitsSource -import com.twitter.follow_recommendations.common.candidate_sources.salsa.RecentEngagementDirectFollowSalsaExpansionSource -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentEngagementSimilarUsersSource -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentFollowingSimilarUsersSource -import com.twitter.follow_recommendations.common.candidate_sources.sims.Follow2vecNearestNeighborsStore -import com.twitter.follow_recommendations.common.candidate_sources.stp.BaseOnlineSTPSource -import com.twitter.follow_recommendations.common.candidate_sources.stp.OfflineStrongTiePredictionSource -import com.twitter.follow_recommendations.common.candidate_sources.top_organic_follows_accounts.TopOrganicFollowsAccountsSource -import com.twitter.follow_recommendations.common.candidate_sources.triangular_loops.TriangularLoopsSource -import com.twitter.follow_recommendations.common.candidate_sources.two_hop_random_walk.TwoHopRandomWalkSource -import com.twitter.follow_recommendations.common.candidate_sources.user_user_graph.UserUserGraphCandidateSource -import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlCandidateSourceWeightParams._ -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.timelines.configapi.Params - -object PostNuxMlFlowCandidateSourceWeights { - - def getWeights(params: Params): Map[CandidateSourceIdentifier, Double] = { - Map[CandidateSourceIdentifier, Double]( - // Social based - PPMILocaleFollowSource.Identifier -> params(CandidateWeightPPMILocaleFollow), - Follow2vecNearestNeighborsStore.IdentifierF2vLinearRegression -> params( - CandidateWeightFollow2vecNearestNeighbors), - RecentFollowingSimilarUsersSource.Identifier -> params( - CandidateWeightRecentFollowingSimilarUsers), - BaseOnlineSTPSource.Identifier -> params(CandidateWeightOnlineStp), - OfflineStrongTiePredictionSource.Identifier -> params( - CandidateWeightOfflineStrongTiePrediction), - ForwardEmailBookSource.Identifier -> params(CandidateWeightForwardEmailBook), - ForwardPhoneBookSource.Identifier -> params(CandidateWeightForwardPhoneBook), - ReverseEmailBookSource.Identifier -> params(CandidateWeightReverseEmailBook), - ReversePhoneBookSource.Identifier -> params(CandidateWeightReversePhoneBook), - TriangularLoopsSource.Identifier -> params(CandidateWeightTriangularLoops), - TwoHopRandomWalkSource.Identifier -> params(CandidateWeightTwoHopRandomWalk), - UserUserGraphCandidateSource.Identifier -> params(CandidateWeightUserUserGraph), - // Geo based - PopCountrySource.Identifier -> params(CandidateWeightPopCountry), - PopCountryBackFillSource.Identifier -> params(CandidateWeightPopGeoBackfill), - PopGeohashSource.Identifier -> params(CandidateWeightPopGeohash), - PopGeohashQualityFollowSource.Identifier -> params(CandidateWeightPopGeohashQualityFollow), - CrowdSearchAccountsSource.Identifier -> params(CandidateWeightCrowdSearch), - TopOrganicFollowsAccountsSource.Identifier -> params(CandidateWeightTopOrganicFollow), - // Engagement based - RealGraphOonV2Source.Identifier -> params(CandidateWeightRealGraphOonV2), - RecentEngagementNonDirectFollowSource.Identifier -> params( - CandidateWeightRecentEngagementNonDirectFollow), - RecentEngagementSimilarUsersSource.Identifier -> params( - CandidateWeightRecentEngagementSimilarUsers), - RepeatedProfileVisitsSource.Identifier -> params(CandidateWeightRepeatedProfileVisits), - RecentEngagementDirectFollowSalsaExpansionSource.Identifier -> params( - CandidateWeightRecentEngagementDirectFollowSalsaExpansion), - ) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.scala deleted file mode 100644 index f329cbd13..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys.scala +++ /dev/null @@ -1,46 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -object PostNuxMlFlowCandidateSourceWeightsFeatureSwitchKeys { - val CandidateWeightCrowdSearch = "post_nux_ml_flow_candidate_source_weights_user_crowd_search" - val CandidateWeightTopOrganicFollow = - "post_nux_ml_flow_candidate_source_weights_top_organic_follow" - val CandidateWeightPPMILocaleFollow = - "post_nux_ml_flow_candidate_source_weights_user_ppmi_locale_follow" - val CandidateWeightForwardEmailBook = - "post_nux_ml_flow_candidate_source_weights_user_forward_email_book" - val CandidateWeightForwardPhoneBook = - "post_nux_ml_flow_candidate_source_weights_user_forward_phone_book" - val CandidateWeightOfflineStrongTiePrediction = - "post_nux_ml_flow_candidate_source_weights_user_offline_strong_tie_prediction" - val CandidateWeightOnlineStp = "post_nux_ml_flow_candidate_source_weights_user_online_stp" - val CandidateWeightPopCountry = "post_nux_ml_flow_candidate_source_weights_user_pop_country" - val CandidateWeightPopGeohash = "post_nux_ml_flow_candidate_source_weights_user_pop_geohash" - val CandidateWeightPopGeohashQualityFollow = - "post_nux_ml_flow_candidate_source_weights_user_pop_geohash_quality_follow" - val CandidateWeightPopGeoBackfill = - "post_nux_ml_flow_candidate_source_weights_user_pop_geo_backfill" - val CandidateWeightRecentFollowingSimilarUsers = - "post_nux_ml_flow_candidate_source_weights_user_recent_following_similar_users" - val CandidateWeightRecentEngagementDirectFollowSalsaExpansion = - "post_nux_ml_flow_candidate_source_weights_user_recent_engagement_direct_follow_salsa_expansion" - val CandidateWeightRecentEngagementNonDirectFollow = - "post_nux_ml_flow_candidate_source_weights_user_recent_engagement_non_direct_follow" - val CandidateWeightRecentEngagementSimilarUsers = - "post_nux_ml_flow_candidate_source_weights_user_recent_engagement_similar_users" - val CandidateWeightRepeatedProfileVisits = - "post_nux_ml_flow_candidate_source_weights_user_repeated_profile_visits" - val CandidateWeightFollow2vecNearestNeighbors = - "post_nux_ml_flow_candidate_source_weights_user_follow2vec_nearest_neighbors" - val CandidateWeightReverseEmailBook = - "post_nux_ml_flow_candidate_source_weights_user_reverse_email_book" - val CandidateWeightReversePhoneBook = - "post_nux_ml_flow_candidate_source_weights_user_reverse_phone_book" - val CandidateWeightTriangularLoops = - "post_nux_ml_flow_candidate_source_weights_user_triangular_loops" - val CandidateWeightTwoHopRandomWalk = - "post_nux_ml_flow_candidate_source_weights_user_two_hop_random_walk" - val CandidateWeightUserUserGraph = - "post_nux_ml_flow_candidate_source_weights_user_user_user_graph" - val CandidateWeightRealGraphOonV2 = - "post_nux_ml_flow_candidate_source_weights_user_real_graph_oon_v2" -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowFSConfig.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowFSConfig.scala deleted file mode 100644 index 0dd059dad..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowFSConfig.scala +++ /dev/null @@ -1,80 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker.NoShuffle -import com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker.RandomShuffler -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.timelines.configapi.Param -import com.twitter.util.Duration -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PostNuxMlFlowFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = Seq( - PostNuxMlParams.OnlineSTPEnabled, - PostNuxMlParams.SamplingTransformEnabled, - PostNuxMlParams.Follow2VecLinearRegressionEnabled, - PostNuxMlParams.UseMlRanker, - PostNuxMlParams.EnableCandidateParamHydration, - PostNuxMlParams.EnableInterleaveRanker, - PostNuxMlParams.EnableAdhocRanker, - PostNuxMlParams.ExcludeNearZeroCandidates, - PostNuxMlParams.IncludeRepeatedProfileVisitsCandidateSource, - PostNuxMlParams.EnableInterestsOptOutPredicate, - PostNuxMlParams.EnableSGSPredicate, - PostNuxMlParams.EnableInvalidRelationshipPredicate, - PostNuxMlParams.EnableRemoveAccountProofTransform, - PostNuxMlParams.EnablePPMILocaleFollowSourceInPostNux, - PostNuxMlParams.EnableRealGraphOonV2, - PostNuxMlParams.GetFollowersFromSgs, - PostNuxMlRequestBuilderParams.EnableInvalidRelationshipPredicate - ) - - override val doubleFSParams: Seq[FSBoundedParam[Double]] = Seq( - PostNuxMlCandidateSourceWeightParams.CandidateWeightCrowdSearch, - PostNuxMlCandidateSourceWeightParams.CandidateWeightTopOrganicFollow, - PostNuxMlCandidateSourceWeightParams.CandidateWeightPPMILocaleFollow, - PostNuxMlCandidateSourceWeightParams.CandidateWeightForwardEmailBook, - PostNuxMlCandidateSourceWeightParams.CandidateWeightForwardPhoneBook, - PostNuxMlCandidateSourceWeightParams.CandidateWeightOfflineStrongTiePrediction, - PostNuxMlCandidateSourceWeightParams.CandidateWeightOnlineStp, - PostNuxMlCandidateSourceWeightParams.CandidateWeightPopCountry, - PostNuxMlCandidateSourceWeightParams.CandidateWeightPopGeohash, - PostNuxMlCandidateSourceWeightParams.CandidateWeightPopGeohashQualityFollow, - PostNuxMlCandidateSourceWeightParams.CandidateWeightPopGeoBackfill, - PostNuxMlCandidateSourceWeightParams.CandidateWeightRecentFollowingSimilarUsers, - PostNuxMlCandidateSourceWeightParams.CandidateWeightRecentEngagementDirectFollowSalsaExpansion, - PostNuxMlCandidateSourceWeightParams.CandidateWeightRecentEngagementNonDirectFollow, - PostNuxMlCandidateSourceWeightParams.CandidateWeightRecentEngagementSimilarUsers, - PostNuxMlCandidateSourceWeightParams.CandidateWeightRepeatedProfileVisits, - PostNuxMlCandidateSourceWeightParams.CandidateWeightFollow2vecNearestNeighbors, - PostNuxMlCandidateSourceWeightParams.CandidateWeightReverseEmailBook, - PostNuxMlCandidateSourceWeightParams.CandidateWeightReversePhoneBook, - PostNuxMlCandidateSourceWeightParams.CandidateWeightTriangularLoops, - PostNuxMlCandidateSourceWeightParams.CandidateWeightTwoHopRandomWalk, - PostNuxMlCandidateSourceWeightParams.CandidateWeightUserUserGraph, - PostNuxMlCandidateSourceWeightParams.CandidateWeightRealGraphOonV2, - PostNuxMlParams.TurnoffMLScorerQFThreshold - ) - - override val durationFSParams: Seq[FSBoundedParam[Duration] with HasDurationConversion] = Seq( - PostNuxMlParams.MlRankerBudget, - PostNuxMlRequestBuilderParams.TopicIdFetchBudget, - PostNuxMlRequestBuilderParams.DismissedIdScanBudget, - PostNuxMlRequestBuilderParams.WTFImpressionsScanBudget - ) - - override val gatedOverridesMap = Map( - PostNuxMlFlowFeatureSwitchKeys.EnableRandomDataCollection -> Seq( - PostNuxMlParams.CandidateShuffler := new RandomShuffler[CandidateUser], - PostNuxMlParams.LogRandomRankerId := true - ), - PostNuxMlFlowFeatureSwitchKeys.EnableNoShuffler -> Seq( - PostNuxMlParams.CandidateShuffler := new NoShuffle[CandidateUser] - ), - ) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowFeatureSwitchKeys.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowFeatureSwitchKeys.scala deleted file mode 100644 index 6a44c4bbb..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlFlowFeatureSwitchKeys.scala +++ /dev/null @@ -1,27 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -object PostNuxMlFlowFeatureSwitchKeys { - val UseMlRanker = "post_nux_ml_flow_use_ml_ranker" - val EnableCandidateParamHydration = "post_nux_ml_flow_enable_candidate_param_hydration" - val OnlineSTPEnabled = "post_nux_ml_flow_online_stp_source_enabled" - val Follow2VecLinearRegressionEnabled = "post_nux_ml_flow_follow_to_vec_lr_source_enabled" - val EnableRandomDataCollection = "post_nux_ml_flow_random_data_collection_enabled" - val EnableAdhocRanker = "post_nux_ml_flow_adhoc_ranker_enabled" - val EnableFatigueRanker = "post_nux_ml_flow_fatigue_ranker_enabled" - val EnableInterleaveRanker = "post_nux_ml_flow_interleave_ranker_enabled" - val IncludeRepeatedProfileVisitsCandidateSource = - "post_nux_ml_flow_include_repeated_profile_visits_candidate_source" - val MLRankerBudget = "post_nux_ml_flow_ml_ranker_budget_millis" - val EnableNoShuffler = "post_nux_ml_flow_no_shuffler" - val SamplingTransformEnabled = "post_nux_ml_flow_sampling_transform_enabled" - val ExcludeNearZeroCandidates = "post_nux_ml_flow_exclude_near_zero_candidates" - val EnableInterestsOptOutPredicate = "post_nux_ml_flow_enable_interests_opt_out_predicate" - val EnableRemoveAccountProofTransform = "post_nux_ml_flow_enable_remove_account_proof_transform" - val EnablePPMILocaleFollowSourceInPostNux = "post_nux_ml_flow_enable_ppmilocale_follow_source" - val EnableInvalidRelationshipPredicate = "post_nux_ml_flow_enable_invalid_relationship_predicate" - val EnableRealGraphOonV2 = "post_nux_ml_flow_enable_real_graph_oon_v2" - val EnableSGSPredicate = "post_nux_ml_flow_enable_sgs_predicate" - val EnableHssPredicate = "post_nux_ml_flow_enable_hss_predicate" - val GetFollowersFromSgs = "post_nux_ml_flow_get_followers_from_sgs" - val TurnOffMLScorerQFThreshold = "post_nux_ml_flow_turn_off_ml_scorer_threhsold" -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlParams.scala deleted file mode 100644 index cb5cf3648..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlParams.scala +++ /dev/null @@ -1,133 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -import com.twitter.conversions.DurationOps._ -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker.CandidateShuffler -import com.twitter.follow_recommendations.common.rankers.weighted_candidate_source_ranker.ExponentialShuffler -import com.twitter.timelines.configapi.DurationConversion -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.timelines.configapi.Param -import com.twitter.util.Duration - -abstract class PostNuxMlParams[A](default: A) extends Param[A](default) { - override val statName: String = "post_nux_ml/" + this.getClass.getSimpleName -} - -object PostNuxMlParams { - - // infra params: - case object FetchCandidateSourceBudget extends PostNuxMlParams[Duration](90.millisecond) - - // WTF Impression Store has very high tail latency (p9990 or p9999), but p99 latency is pretty good (~100ms) - // set the time budget for this step to be 200ms to make the performance of service more predictable - case object FatigueRankerBudget extends PostNuxMlParams[Duration](200.millisecond) - - case object MlRankerBudget - extends FSBoundedParam[Duration]( - name = PostNuxMlFlowFeatureSwitchKeys.MLRankerBudget, - default = 400.millisecond, - min = 100.millisecond, - max = 800.millisecond) - with HasDurationConversion { - override val durationConversion: DurationConversion = DurationConversion.FromMillis - } - - // product params: - case object TargetEligibility extends PostNuxMlParams[Boolean](true) - - case object ResultSizeParam extends PostNuxMlParams[Int](3) - case object BatchSizeParam extends PostNuxMlParams[Int](12) - - case object CandidateShuffler - extends PostNuxMlParams[CandidateShuffler[CandidateUser]]( - new ExponentialShuffler[CandidateUser]) - case object LogRandomRankerId extends PostNuxMlParams[Boolean](false) - - // whether or not to use the ml ranker at all (feature hydration + ranker) - case object UseMlRanker - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.UseMlRanker, false) - - // whether or not to enable candidate param hydration in postnux_ml_flow - case object EnableCandidateParamHydration - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.EnableCandidateParamHydration, false) - - // Whether or not OnlineSTP candidates are considered in the final pool of candidates. - // If set to `false`, the candidate source will be removed *after* all other considerations. - case object OnlineSTPEnabled - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.OnlineSTPEnabled, false) - - // Whether or not the candidates are sampled from a Plackett-Luce model - case object SamplingTransformEnabled - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.SamplingTransformEnabled, false) - - // Whether or not Follow2Vec candidates are considered in the final pool of candidates. - // If set to `false`, the candidate source will be removed *after* all other considerations. - case object Follow2VecLinearRegressionEnabled - extends FSParam[Boolean]( - PostNuxMlFlowFeatureSwitchKeys.Follow2VecLinearRegressionEnabled, - false) - - // Whether or not to enable AdhocRanker to allow adhoc, non-ML, score modifications. - case object EnableAdhocRanker - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.EnableAdhocRanker, false) - - // Whether the impression-based fatigue ranker is enabled or not. - case object EnableFatigueRanker - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.EnableFatigueRanker, true) - - // whether or not to enable InterleaveRanker for producer-side experiments. - case object EnableInterleaveRanker - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.EnableInterleaveRanker, false) - - // whether to exclude users in near zero user state - case object ExcludeNearZeroCandidates - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.ExcludeNearZeroCandidates, false) - - case object EnablePPMILocaleFollowSourceInPostNux - extends FSParam[Boolean]( - PostNuxMlFlowFeatureSwitchKeys.EnablePPMILocaleFollowSourceInPostNux, - false) - - case object EnableInterestsOptOutPredicate - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.EnableInterestsOptOutPredicate, false) - - case object EnableInvalidRelationshipPredicate - extends FSParam[Boolean]( - PostNuxMlFlowFeatureSwitchKeys.EnableInvalidRelationshipPredicate, - false) - - // Totally disabling SGS predicate need to disable EnableInvalidRelationshipPredicate as well - case object EnableSGSPredicate - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.EnableSGSPredicate, true) - - case object EnableHssPredicate - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.EnableHssPredicate, true) - - // Whether or not to include RepeatedProfileVisits as one of the candidate sources in the PostNuxMlFlow. If false, - // RepeatedProfileVisitsSource would not be run for the users in candidate_generation. - case object IncludeRepeatedProfileVisitsCandidateSource - extends FSParam[Boolean]( - PostNuxMlFlowFeatureSwitchKeys.IncludeRepeatedProfileVisitsCandidateSource, - false) - - case object EnableRealGraphOonV2 - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.EnableRealGraphOonV2, false) - - case object GetFollowersFromSgs - extends FSParam[Boolean](PostNuxMlFlowFeatureSwitchKeys.GetFollowersFromSgs, false) - - case object EnableRemoveAccountProofTransform - extends FSParam[Boolean]( - PostNuxMlFlowFeatureSwitchKeys.EnableRemoveAccountProofTransform, - false) - - // quality factor threshold to turn off ML ranker completely - object TurnoffMLScorerQFThreshold - extends FSBoundedParam[Double]( - name = PostNuxMlFlowFeatureSwitchKeys.TurnOffMLScorerQFThreshold, - default = 0.3, - min = 0.1, - max = 1.0) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequest.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequest.scala deleted file mode 100644 index 2cb112638..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequest.scala +++ /dev/null @@ -1,54 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -import com.twitter.core_workflows.user_model.thriftscala.UserState -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.models._ -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Params - -case class PostNuxMlRequest( - override val params: Params, - override val clientContext: ClientContext, - override val similarToUserIds: Seq[Long], - inputExcludeUserIds: Seq[Long], - override val recentFollowedUserIds: Option[Seq[Long]], - override val invalidRelationshipUserIds: Option[Set[Long]], - override val recentFollowedByUserIds: Option[Seq[Long]], - override val dismissedUserIds: Option[Seq[Long]], - override val displayLocation: DisplayLocation, - maxResults: Option[Int] = None, - override val debugOptions: Option[DebugOptions] = None, - override val wtfImpressions: Option[Seq[WtfImpression]], - override val uttInterestIds: Option[Seq[Long]] = None, - override val customInterests: Option[Seq[String]] = None, - override val geohashAndCountryCode: Option[GeohashAndCountryCode] = None, - inputPreviouslyRecommendedUserIds: Option[Set[Long]] = None, - inputPreviouslyFollowedUserIds: Option[Set[Long]] = None, - override val isSoftUser: Boolean = false, - override val userState: Option[UserState] = None, - override val qualityFactor: Option[Double] = None) - extends HasParams - with HasSimilarToContext - with HasClientContext - with HasExcludedUserIds - with HasDisplayLocation - with HasDebugOptions - with HasGeohashAndCountryCode - with HasPreFetchedFeature - with HasDismissedUserIds - with HasInterestIds - with HasPreviousRecommendationsContext - with HasIsSoftUser - with HasUserState - with HasInvalidRelationshipUserIds - with HasQualityFactor { - override val excludedUserIds: Seq[Long] = { - inputExcludeUserIds ++ clientContext.userId.toSeq ++ similarToUserIds - } - override val previouslyRecommendedUserIDs: Set[Long] = - inputPreviouslyRecommendedUserIds.getOrElse(Set.empty) - override val previouslyFollowedUserIds: Set[Long] = - inputPreviouslyFollowedUserIds.getOrElse(Set.empty) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequestBuilder.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequestBuilder.scala deleted file mode 100644 index aeb248b7f..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequestBuilder.scala +++ /dev/null @@ -1,173 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.clients.dismiss_store.DismissStore -import com.twitter.follow_recommendations.common.clients.geoduck.UserLocationFetcher -import com.twitter.follow_recommendations.common.clients.impression_store.WtfImpressionStore -import com.twitter.follow_recommendations.common.clients.interests_service.InterestServiceClient -import com.twitter.follow_recommendations.common.clients.socialgraph.SocialGraphClient -import com.twitter.follow_recommendations.common.clients.user_state.UserStateClient -import com.twitter.follow_recommendations.common.predicates.dismiss.DismissedCandidatePredicateParams -import com.twitter.follow_recommendations.common.utils.RescueWithStatsUtils._ -import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlRequestBuilderParams.DismissedIdScanBudget -import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlRequestBuilderParams.TopicIdFetchBudget -import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlRequestBuilderParams.WTFImpressionsScanBudget -import com.twitter.follow_recommendations.products.common.ProductRequest -import com.twitter.inject.Logging -import com.twitter.stitch.Stitch -import com.twitter.util.Time -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class PostNuxMlRequestBuilder @Inject() ( - socialGraph: SocialGraphClient, - wtfImpressionStore: WtfImpressionStore, - dismissStore: DismissStore, - userLocationFetcher: UserLocationFetcher, - interestServiceClient: InterestServiceClient, - userStateClient: UserStateClient, - statsReceiver: StatsReceiver) - extends Logging { - - val stats: StatsReceiver = statsReceiver.scope("post_nux_ml_request_builder") - val invalidRelationshipUsersStats: StatsReceiver = stats.scope("invalidRelationshipUserIds") - private val invalidRelationshipUsersMaxSizeCounter = - invalidRelationshipUsersStats.counter("maxSize") - private val invalidRelationshipUsersNotMaxSizeCounter = - invalidRelationshipUsersStats.counter("notMaxSize") - - def build( - req: ProductRequest, - previouslyRecommendedUserIds: Option[Set[Long]] = None, - previouslyFollowedUserIds: Option[Set[Long]] = None - ): Stitch[PostNuxMlRequest] = { - val dl = req.recommendationRequest.displayLocation - val resultsStitch = Stitch.collect( - req.recommendationRequest.clientContext.userId - .map { userId => - val lookBackDuration = req.params(DismissedCandidatePredicateParams.LookBackDuration) - val negativeStartTs = -(Time.now - lookBackDuration).inMillis - val recentFollowedUserIdsStitch = - rescueWithStats( - socialGraph.getRecentFollowedUserIds(userId), - stats, - "recentFollowedUserIds") - val invalidRelationshipUserIdsStitch = - if (req.params(PostNuxMlParams.EnableInvalidRelationshipPredicate)) { - rescueWithStats( - socialGraph - .getInvalidRelationshipUserIds(userId) - .onSuccess(ids => - if (ids.size >= SocialGraphClient.MaxNumInvalidRelationship) { - invalidRelationshipUsersMaxSizeCounter.incr() - } else { - invalidRelationshipUsersNotMaxSizeCounter.incr() - }), - stats, - "invalidRelationshipUserIds" - ) - } else { - Stitch.value(Seq.empty) - } - // recentFollowedByUserIds are only used in experiment candidate sources - val recentFollowedByUserIdsStitch = if (req.params(PostNuxMlParams.GetFollowersFromSgs)) { - rescueWithStats( - socialGraph.getRecentFollowedByUserIdsFromCachedColumn(userId), - stats, - "recentFollowedByUserIds") - } else Stitch.value(Seq.empty) - val wtfImpressionsStitch = - rescueWithStatsWithin( - wtfImpressionStore.get(userId, dl), - stats, - "wtfImpressions", - req.params(WTFImpressionsScanBudget)) - val dismissedUserIdsStitch = - rescueWithStatsWithin( - dismissStore.get(userId, negativeStartTs, None), - stats, - "dismissedUserIds", - req.params(DismissedIdScanBudget)) - val locationStitch = - rescueOptionalWithStats( - userLocationFetcher.getGeohashAndCountryCode( - Some(userId), - req.recommendationRequest.clientContext.ipAddress), - stats, - "userLocation" - ) - val topicIdsStitch = - rescueWithStatsWithin( - interestServiceClient.fetchUttInterestIds(userId), - stats, - "topicIds", - req.params(TopicIdFetchBudget)) - val userStateStitch = - rescueOptionalWithStats(userStateClient.getUserState(userId), stats, "userState") - Stitch.join( - recentFollowedUserIdsStitch, - invalidRelationshipUserIdsStitch, - recentFollowedByUserIdsStitch, - dismissedUserIdsStitch, - wtfImpressionsStitch, - locationStitch, - topicIdsStitch, - userStateStitch - ) - }) - - resultsStitch.map { - case Some( - ( - recentFollowedUserIds, - invalidRelationshipUserIds, - recentFollowedByUserIds, - dismissedUserIds, - wtfImpressions, - locationInfo, - topicIds, - userState)) => - PostNuxMlRequest( - params = req.params, - clientContext = req.recommendationRequest.clientContext, - similarToUserIds = Nil, - inputExcludeUserIds = req.recommendationRequest.excludedIds.getOrElse(Nil), - recentFollowedUserIds = Some(recentFollowedUserIds), - invalidRelationshipUserIds = Some(invalidRelationshipUserIds.toSet), - recentFollowedByUserIds = Some(recentFollowedByUserIds), - dismissedUserIds = Some(dismissedUserIds), - displayLocation = dl, - maxResults = req.recommendationRequest.maxResults, - debugOptions = req.recommendationRequest.debugParams.flatMap(_.debugOptions), - wtfImpressions = Some(wtfImpressions), - geohashAndCountryCode = locationInfo, - uttInterestIds = Some(topicIds), - inputPreviouslyRecommendedUserIds = previouslyRecommendedUserIds, - inputPreviouslyFollowedUserIds = previouslyFollowedUserIds, - isSoftUser = req.recommendationRequest.isSoftUser, - userState = userState - ) - case _ => - PostNuxMlRequest( - params = req.params, - clientContext = req.recommendationRequest.clientContext, - similarToUserIds = Nil, - inputExcludeUserIds = req.recommendationRequest.excludedIds.getOrElse(Nil), - recentFollowedUserIds = None, - invalidRelationshipUserIds = None, - recentFollowedByUserIds = None, - dismissedUserIds = None, - displayLocation = dl, - maxResults = req.recommendationRequest.maxResults, - debugOptions = req.recommendationRequest.debugParams.flatMap(_.debugOptions), - wtfImpressions = None, - geohashAndCountryCode = None, - inputPreviouslyRecommendedUserIds = previouslyRecommendedUserIds, - inputPreviouslyFollowedUserIds = previouslyFollowedUserIds, - isSoftUser = req.recommendationRequest.isSoftUser, - userState = None - ) - } - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequestBuilderParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequestBuilderParams.scala deleted file mode 100644 index da60f0382..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml/PostNuxMlRequestBuilderParams.scala +++ /dev/null @@ -1,45 +0,0 @@ -package com.twitter.follow_recommendations.flows.post_nux_ml - -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.util.Duration -import com.twitter.conversions.DurationOps._ -import com.twitter.timelines.configapi.DurationConversion -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.HasDurationConversion - -object PostNuxMlRequestBuilderParams { - case object TopicIdFetchBudget - extends FSBoundedParam[Duration]( - name = "post_nux_ml_request_builder_topic_id_fetch_budget_millis", - default = 200.millisecond, - min = 80.millisecond, - max = 400.millisecond) - with HasDurationConversion { - override val durationConversion: DurationConversion = DurationConversion.FromMillis - } - - case object DismissedIdScanBudget - extends FSBoundedParam[Duration]( - name = "post_nux_ml_request_builder_dismissed_id_scan_budget_millis", - default = 200.millisecond, - min = 80.millisecond, - max = 400.millisecond) - with HasDurationConversion { - override val durationConversion: DurationConversion = DurationConversion.FromMillis - } - - case object WTFImpressionsScanBudget - extends FSBoundedParam[Duration]( - name = "post_nux_ml_request_builder_wtf_impressions_scan_budget_millis", - default = 200.millisecond, - min = 80.millisecond, - max = 400.millisecond) - with HasDurationConversion { - override val durationConversion: DurationConversion = DurationConversion.FromMillis - } - - case object EnableInvalidRelationshipPredicate - extends FSParam[Boolean]( - name = "post_nux_ml_request_builder_enable_invalid_relationship_predicate", - false) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/logging/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/logging/BUILD deleted file mode 100644 index a35992e93..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/logging/BUILD +++ /dev/null @@ -1,18 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models", - "follow-recommendations-service/thrift/src/main/thrift:thrift-scala", - "scribelib/marshallers/src/main/scala/com/twitter/scribelib/marshallers", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/logging/FrsLogger.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/logging/FrsLogger.scala deleted file mode 100644 index 8b920c556..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/logging/FrsLogger.scala +++ /dev/null @@ -1,164 +0,0 @@ -package com.twitter.follow_recommendations.logging - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.follow_recommendations.common.models.HasIsSoftUser -import com.twitter.follow_recommendations.configapi.params.GlobalParams -import com.twitter.follow_recommendations.logging.thriftscala.RecommendationLog -import com.twitter.follow_recommendations.models.DebugParams -import com.twitter.follow_recommendations.models.RecommendationFlowData -import com.twitter.follow_recommendations.models.RecommendationRequest -import com.twitter.follow_recommendations.models.RecommendationResponse -import com.twitter.follow_recommendations.models.ScoringUserRequest -import com.twitter.follow_recommendations.models.ScoringUserResponse -import com.twitter.inject.annotations.Flag -import com.twitter.logging.LoggerFactory -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.scribelib.marshallers.ClientDataProvider -import com.twitter.scribelib.marshallers.ExternalRefererDataProvider -import com.twitter.scribelib.marshallers.ScribeSerialization -import com.twitter.timelines.configapi.HasParams -import com.twitter.util.Time -import javax.inject.Inject -import javax.inject.Named -import javax.inject.Singleton - -/** - * This is the standard logging class we use to log data into: - * 1) logs.follow_recommendations_logs - * - * This logger logs data for 2 endpoints: getRecommendations, scoreUserCandidates - * All data scribed via this logger have to be converted into the same thrift type: RecommendationLog - * - * 2) logs.frs_recommendation_flow_logs - * - * This logger logs recommendation flow data for getRecommendations requests - * All data scribed via this logger have to be converted into the same thrift type: FrsRecommendationFlowLog - */ -@Singleton -class FrsLogger @Inject() ( - @Named(GuiceNamedConstants.REQUEST_LOGGER) loggerFactory: LoggerFactory, - @Named(GuiceNamedConstants.FLOW_LOGGER) flowLoggerFactory: LoggerFactory, - stats: StatsReceiver, - @Flag("log_results") serviceShouldLogResults: Boolean) - extends ScribeSerialization { - private val logger = loggerFactory.apply() - private val flowLogger = flowLoggerFactory.apply() - private val logRecommendationCounter = stats.counter("scribe_recommendation") - private val logScoringCounter = stats.counter("scribe_scoring") - private val logRecommendationFlowCounter = stats.counter("scribe_recommendation_flow") - - def logRecommendationResult( - request: RecommendationRequest, - response: RecommendationResponse - ): Unit = { - if (!request.isSoftUser) { - val log = - RecommendationLog(request.toOfflineThrift, response.toOfflineThrift, Time.now.inMillis) - logRecommendationCounter.incr() - logger.info( - serializeThrift( - log, - FrsLogger.LogCategory, - FrsLogger.mkProvider(request.clientContext) - )) - } - } - - def logScoringResult(request: ScoringUserRequest, response: ScoringUserResponse): Unit = { - if (!request.isSoftUser) { - val log = - RecommendationLog( - request.toRecommendationRequest.toOfflineThrift, - response.toRecommendationResponse.toOfflineThrift, - Time.now.inMillis) - logScoringCounter.incr() - logger.info( - serializeThrift( - log, - FrsLogger.LogCategory, - FrsLogger.mkProvider(request.toRecommendationRequest.clientContext) - )) - } - } - - def logRecommendationFlowData[Target <: HasClientContext with HasIsSoftUser with HasParams]( - request: Target, - flowData: RecommendationFlowData[Target] - ): Unit = { - if (!request.isSoftUser && request.params(GlobalParams.EnableRecommendationFlowLogs)) { - val log = flowData.toRecommendationFlowLogOfflineThrift - logRecommendationFlowCounter.incr() - flowLogger.info( - serializeThrift( - log, - FrsLogger.FlowLogCategory, - FrsLogger.mkProvider(request.clientContext) - )) - } - } - - // We prefer the settings given in the user request, and if none provided we default to the - // aurora service configuration. - def shouldLog(debugParamsOpt: Option[DebugParams]): Boolean = - debugParamsOpt match { - case Some(debugParams) => - debugParams.debugOptions match { - case Some(debugOptions) => - !debugOptions.doNotLog - case None => - serviceShouldLogResults - } - case None => - serviceShouldLogResults - } - -} - -object FrsLogger { - val LogCategory = "follow_recommendations_logs" - val FlowLogCategory = "frs_recommendation_flow_logs" - - def mkProvider(clientContext: ClientContext) = new ClientDataProvider { - - /** The id of the current user. When the user is logged out, this method should return None. */ - override val userId: Option[Long] = clientContext.userId - - /** The id of the guest, which is present in logged-in or loged-out states */ - override val guestId: Option[Long] = clientContext.guestId - - /** The personalization id (pid) of the user, used to personalize Twitter services */ - override val personalizationId: Option[String] = None - - /** The id of the individual device the user is currently using. This id will be unique for different users' devices. */ - override val deviceId: Option[String] = clientContext.deviceId - - /** The OAuth application id of the application the user is currently using */ - override val clientApplicationId: Option[Long] = clientContext.appId - - /** The OAuth parent application id of the application the user is currently using */ - override val parentApplicationId: Option[Long] = None - - /** The two-letter, upper-case country code used to designate the country from which the scribe event occurred */ - override val countryCode: Option[String] = clientContext.countryCode - - /** The two-letter, lower-case language code used to designate the probably language spoken by the scribe event initiator */ - override val languageCode: Option[String] = clientContext.languageCode - - /** The user-agent header used to identify the client browser or device that the user is currently active on */ - override val userAgent: Option[String] = clientContext.userAgent - - /** Whether the user is accessing Twitter via a secured connection */ - override val isSsl: Option[Boolean] = Some(true) - - /** The referring URL to the current page for web-based clients, if applicable */ - override val referer: Option[String] = None - - /** - * The external site, partner, or email that lead to the current Twitter application. Returned value consists of a - * tuple including the encrypted referral data and the type of referral - */ - override val externalReferer: Option[ExternalRefererDataProvider] = None - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/BUILD deleted file mode 100644 index 597ab76c4..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "configapi/configapi-core/src/main/scala/com/twitter/timelines/configapi", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/common", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models", - "follow-recommendations-service/thrift/src/main/thrift:thrift-scala", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/model/marshalling/request", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/CandidateSourceType.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/CandidateSourceType.scala deleted file mode 100644 index 38215c44b..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/CandidateSourceType.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.models - -object CandidateSourceType extends Enumeration { - type CandidateSourceType = Value - val Social = Value("social") - val GeoAndInterests = Value("geo_and_interests") - val ActivityContextual = Value("activity_contextual") - val None = Value("none") -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/CandidateUserDebugParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/CandidateUserDebugParams.scala deleted file mode 100644 index a5702b2b3..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/CandidateUserDebugParams.scala +++ /dev/null @@ -1,5 +0,0 @@ -package com.twitter.follow_recommendations.models - -import com.twitter.timelines.configapi.Params - -case class CandidateUserDebugParams(paramsMap: Map[Long, Params]) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/DebugParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/DebugParams.scala deleted file mode 100644 index dee7f9b6a..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/DebugParams.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.twitter.follow_recommendations.models - -import com.twitter.follow_recommendations.common.models.DebugOptions -import com.twitter.follow_recommendations.common.models.DebugOptions.fromDebugParamsThrift -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.{thriftscala => t} -import com.twitter.timelines.configapi.{FeatureValue => ConfigApiFeatureValue} - -case class DebugParams( - featureOverrides: Option[Map[String, ConfigApiFeatureValue]], - debugOptions: Option[DebugOptions]) - -object DebugParams { - def fromThrift(thrift: t.DebugParams): DebugParams = DebugParams( - featureOverrides = thrift.featureOverrides.map { map => - map.mapValues(FeatureValue.fromThrift).toMap - }, - debugOptions = Some( - fromDebugParamsThrift(thrift) - ) - ) - def toOfflineThrift(model: DebugParams): offline.OfflineDebugParams = - offline.OfflineDebugParams(randomizationSeed = model.debugOptions.flatMap(_.randomizationSeed)) -} - -trait HasFrsDebugParams { - def frsDebugParams: Option[DebugParams] -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/DisplayContext.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/DisplayContext.scala deleted file mode 100644 index 59f0adfd7..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/DisplayContext.scala +++ /dev/null @@ -1,113 +0,0 @@ -package com.twitter.follow_recommendations.models - -import com.twitter.follow_recommendations.common.models.FlowContext -import com.twitter.follow_recommendations.common.models.RecentlyEngagedUserId -import com.twitter.follow_recommendations.logging.thriftscala.OfflineDisplayContext -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.{thriftscala => t} -import scala.reflect.ClassTag -import scala.reflect.classTag - -trait DisplayContext { - def toOfflineThrift: offline.OfflineDisplayContext -} - -object DisplayContext { - case class Profile(profileId: Long) extends DisplayContext { - override val toOfflineThrift: OfflineDisplayContext = - offline.OfflineDisplayContext.Profile(offline.OfflineProfile(profileId)) - } - case class Search(searchQuery: String) extends DisplayContext { - override val toOfflineThrift: OfflineDisplayContext = - offline.OfflineDisplayContext.Search(offline.OfflineSearch(searchQuery)) - } - case class Rux(focalAuthorId: Long) extends DisplayContext { - override val toOfflineThrift: OfflineDisplayContext = - offline.OfflineDisplayContext.Rux(offline.OfflineRux(focalAuthorId)) - } - - case class Topic(topicId: Long) extends DisplayContext { - override val toOfflineThrift: OfflineDisplayContext = - offline.OfflineDisplayContext.Topic(offline.OfflineTopic(topicId)) - } - - case class ReactiveFollow(followedUserIds: Seq[Long]) extends DisplayContext { - override val toOfflineThrift: OfflineDisplayContext = - offline.OfflineDisplayContext.ReactiveFollow(offline.OfflineReactiveFollow(followedUserIds)) - } - - case class NuxInterests(flowContext: Option[FlowContext], uttInterestIds: Option[Seq[Long]]) - extends DisplayContext { - override val toOfflineThrift: OfflineDisplayContext = - offline.OfflineDisplayContext.NuxInterests( - offline.OfflineNuxInterests(flowContext.map(_.toOfflineThrift))) - } - - case class PostNuxFollowTask(flowContext: Option[FlowContext]) extends DisplayContext { - override val toOfflineThrift: OfflineDisplayContext = - offline.OfflineDisplayContext.PostNuxFollowTask( - offline.OfflinePostNuxFollowTask(flowContext.map(_.toOfflineThrift))) - } - - case class AdCampaignTarget(similarToUserIds: Seq[Long]) extends DisplayContext { - override val toOfflineThrift: OfflineDisplayContext = - offline.OfflineDisplayContext.AdCampaignTarget( - offline.OfflineAdCampaignTarget(similarToUserIds)) - } - - case class ConnectTab( - byfSeedUserIds: Seq[Long], - similarToUserIds: Seq[Long], - engagedUserIds: Seq[RecentlyEngagedUserId]) - extends DisplayContext { - override val toOfflineThrift: OfflineDisplayContext = - offline.OfflineDisplayContext.ConnectTab( - offline.OfflineConnectTab( - byfSeedUserIds, - similarToUserIds, - engagedUserIds.map(user => user.toOfflineThrift))) - } - - case class SimilarToUser(similarToUserId: Long) extends DisplayContext { - override val toOfflineThrift: OfflineDisplayContext = - offline.OfflineDisplayContext.SimilarToUser(offline.OfflineSimilarToUser(similarToUserId)) - } - - def fromThrift(tDisplayContext: t.DisplayContext): DisplayContext = tDisplayContext match { - case t.DisplayContext.Profile(p) => Profile(p.profileId) - case t.DisplayContext.Search(s) => Search(s.searchQuery) - case t.DisplayContext.Rux(r) => Rux(r.focalAuthorId) - case t.DisplayContext.Topic(t) => Topic(t.topicId) - case t.DisplayContext.ReactiveFollow(f) => ReactiveFollow(f.followedUserIds) - case t.DisplayContext.NuxInterests(n) => - NuxInterests(n.flowContext.map(FlowContext.fromThrift), n.uttInterestIds) - case t.DisplayContext.AdCampaignTarget(a) => - AdCampaignTarget(a.similarToUserIds) - case t.DisplayContext.ConnectTab(connect) => - ConnectTab( - connect.byfSeedUserIds, - connect.similarToUserIds, - connect.recentlyEngagedUserIds.map(RecentlyEngagedUserId.fromThrift)) - case t.DisplayContext.SimilarToUser(r) => - SimilarToUser(r.similarToUserId) - case t.DisplayContext.PostNuxFollowTask(p) => - PostNuxFollowTask(p.flowContext.map(FlowContext.fromThrift)) - case t.DisplayContext.UnknownUnionField(t) => - throw new UnknownDisplayContextException(t.field.name) - } - - def getDisplayContextAs[T <: DisplayContext: ClassTag](displayContext: DisplayContext): T = - displayContext match { - case context: T => context - case _ => - throw new UnexpectedDisplayContextTypeException( - displayContext, - classTag[T].getClass.getSimpleName) - } -} - -class UnknownDisplayContextException(name: String) - extends Exception(s"Unknown DisplayContext in Thrift: ${name}") - -class UnexpectedDisplayContextTypeException(displayContext: DisplayContext, expectedType: String) - extends Exception(s"DisplayContext ${displayContext} not of expected type ${expectedType}") diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/FeatureValue.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/FeatureValue.scala deleted file mode 100644 index 66f0afafa..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/FeatureValue.scala +++ /dev/null @@ -1,24 +0,0 @@ -package com.twitter.follow_recommendations.models - -import com.twitter.follow_recommendations.{thriftscala => t} -import com.twitter.timelines.configapi._ - -object FeatureValue { - def fromThrift(thriftFeatureValue: t.FeatureValue): FeatureValue = thriftFeatureValue match { - case t.FeatureValue.PrimitiveValue(t.PrimitiveFeatureValue.BoolValue(bool)) => - BooleanFeatureValue(bool) - case t.FeatureValue.PrimitiveValue(t.PrimitiveFeatureValue.StrValue(string)) => - StringFeatureValue(string) - case t.FeatureValue.PrimitiveValue(t.PrimitiveFeatureValue.IntValue(int)) => - NumberFeatureValue(int) - case t.FeatureValue.PrimitiveValue(t.PrimitiveFeatureValue.LongValue(long)) => - NumberFeatureValue(long) - case t.FeatureValue.PrimitiveValue(t.PrimitiveFeatureValue.UnknownUnionField(field)) => - throw new UnknownFeatureValueException(s"Primitive: ${field.field.name}") - case t.FeatureValue.UnknownUnionField(field) => - throw new UnknownFeatureValueException(field.field.name) - } -} - -class UnknownFeatureValueException(fieldName: String) - extends Exception(s"Unknown FeatureValue name in thrift: ${fieldName}") diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationFlowData.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationFlowData.scala deleted file mode 100644 index 06b19ac46..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationFlowData.scala +++ /dev/null @@ -1,104 +0,0 @@ -package com.twitter.follow_recommendations.models - -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.ClientContextConverter -import com.twitter.follow_recommendations.common.models.HasUserState -import com.twitter.follow_recommendations.common.utils.UserSignupUtil -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.common.identifier.RecommendationPipelineIdentifier -import com.twitter.product_mixer.core.model.marshalling.HasMarshalling -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.util.Time - -case class RecommendationFlowData[Target <: HasClientContext]( - request: Target, - recommendationFlowIdentifier: RecommendationPipelineIdentifier, - candidateSources: Seq[CandidateSource[Target, CandidateUser]], - candidatesFromCandidateSources: Seq[CandidateUser], - mergedCandidates: Seq[CandidateUser], - filteredCandidates: Seq[CandidateUser], - rankedCandidates: Seq[CandidateUser], - transformedCandidates: Seq[CandidateUser], - truncatedCandidates: Seq[CandidateUser], - results: Seq[CandidateUser]) - extends HasMarshalling { - - import RecommendationFlowData._ - - lazy val toRecommendationFlowLogOfflineThrift: offline.RecommendationFlowLog = { - val userMetadata = userToOfflineRecommendationFlowUserMetadata(request) - val signals = userToOfflineRecommendationFlowSignals(request) - val filteredCandidateSourceCandidates = - candidatesToOfflineRecommendationFlowCandidateSourceCandidates( - candidateSources, - filteredCandidates - ) - val rankedCandidateSourceCandidates = - candidatesToOfflineRecommendationFlowCandidateSourceCandidates( - candidateSources, - rankedCandidates - ) - val truncatedCandidateSourceCandidates = - candidatesToOfflineRecommendationFlowCandidateSourceCandidates( - candidateSources, - truncatedCandidates - ) - - offline.RecommendationFlowLog( - ClientContextConverter.toFRSOfflineClientContextThrift(request.clientContext), - userMetadata, - signals, - Time.now.inMillis, - recommendationFlowIdentifier.name, - Some(filteredCandidateSourceCandidates), - Some(rankedCandidateSourceCandidates), - Some(truncatedCandidateSourceCandidates) - ) - } -} - -object RecommendationFlowData { - def userToOfflineRecommendationFlowUserMetadata[Target <: HasClientContext]( - request: Target - ): Option[offline.OfflineRecommendationFlowUserMetadata] = { - val userSignupAge = UserSignupUtil.userSignupAge(request).map(_.inDays) - val userState = request match { - case req: HasUserState => req.userState.map(_.name) - case _ => None - } - Some(offline.OfflineRecommendationFlowUserMetadata(userSignupAge, userState)) - } - - def userToOfflineRecommendationFlowSignals[Target <: HasClientContext]( - request: Target - ): Option[offline.OfflineRecommendationFlowSignals] = { - val countryCode = request.getCountryCode - Some(offline.OfflineRecommendationFlowSignals(countryCode)) - } - - def candidatesToOfflineRecommendationFlowCandidateSourceCandidates[Target <: HasClientContext]( - candidateSources: Seq[CandidateSource[Target, CandidateUser]], - candidates: Seq[CandidateUser], - ): Seq[offline.OfflineRecommendationFlowCandidateSourceCandidates] = { - val candidatesGroupedByCandidateSources = - candidates.groupBy( - _.getPrimaryCandidateSource.getOrElse(CandidateSourceIdentifier("NoCandidateSource"))) - - candidateSources.map(candidateSource => { - val candidates = - candidatesGroupedByCandidateSources.get(candidateSource.identifier).toSeq.flatten - val candidateUserIds = candidates.map(_.id) - val candidateUserScores = candidates.map(_.score).exists(_.nonEmpty) match { - case true => Some(candidates.map(_.score.getOrElse(-1.0))) - case false => None - } - offline.OfflineRecommendationFlowCandidateSourceCandidates( - candidateSource.identifier.name, - candidateUserIds, - candidateUserScores - ) - }) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationRequest.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationRequest.scala deleted file mode 100644 index fa768b536..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationRequest.scala +++ /dev/null @@ -1,29 +0,0 @@ -package com.twitter.follow_recommendations.models - -import com.twitter.follow_recommendations.common.models.ClientContextConverter -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext - -case class RecommendationRequest( - clientContext: ClientContext, - displayLocation: DisplayLocation, - displayContext: Option[DisplayContext], - maxResults: Option[Int], - cursor: Option[String], - excludedIds: Option[Seq[Long]], - fetchPromotedContent: Option[Boolean], - debugParams: Option[DebugParams] = None, - userLocationState: Option[String] = None, - isSoftUser: Boolean = false) { - def toOfflineThrift: offline.OfflineRecommendationRequest = offline.OfflineRecommendationRequest( - ClientContextConverter.toFRSOfflineClientContextThrift(clientContext), - displayLocation.toOfflineThrift, - displayContext.map(_.toOfflineThrift), - maxResults, - cursor, - excludedIds, - fetchPromotedContent, - debugParams.map(DebugParams.toOfflineThrift) - ) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationResponse.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationResponse.scala deleted file mode 100644 index fadff377b..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/RecommendationResponse.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.twitter.follow_recommendations.models - -import com.twitter.follow_recommendations.{thriftscala => t} -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.common.models.Recommendation -import com.twitter.product_mixer.core.model.marshalling.HasMarshalling - -case class RecommendationResponse(recommendations: Seq[Recommendation]) extends HasMarshalling { - lazy val toThrift: t.RecommendationResponse = - t.RecommendationResponse(recommendations.map(_.toThrift)) - - lazy val toOfflineThrift: offline.OfflineRecommendationResponse = - offline.OfflineRecommendationResponse(recommendations.map(_.toOfflineThrift)) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/Request.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/Request.scala deleted file mode 100644 index a8798bda2..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/Request.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.twitter.follow_recommendations.models - -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.product_mixer.core.model.marshalling.request -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext -import com.twitter.product_mixer.core.model.marshalling.request.ProductContext -import com.twitter.product_mixer.core.model.marshalling.request.{Request => ProductMixerRequest} - -case class Request( - override val maxResults: Option[Int], - override val debugParams: Option[request.DebugParams], - override val productContext: Option[ProductContext], - override val product: request.Product, - override val clientContext: ClientContext, - override val serializedRequestCursor: Option[String], - override val frsDebugParams: Option[DebugParams], - displayLocation: DisplayLocation, - excludedIds: Option[Seq[Long]], - fetchPromotedContent: Option[Boolean], - userLocationState: Option[String] = None) - extends ProductMixerRequest - with HasFrsDebugParams {} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/ScoringUserRequest.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/ScoringUserRequest.scala deleted file mode 100644 index 84d9d3ee3..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/ScoringUserRequest.scala +++ /dev/null @@ -1,45 +0,0 @@ -package com.twitter.follow_recommendations.models - -import com.twitter.follow_recommendations.common.feature_hydration.common.HasPreFetchedFeature -import com.twitter.follow_recommendations.common.models._ -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.product_mixer.core.model.marshalling.request.ClientContext -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.timelines.configapi.HasParams -import com.twitter.timelines.configapi.Params - -case class ScoringUserRequest( - override val clientContext: ClientContext, - override val displayLocation: DisplayLocation, - override val params: Params, - override val debugOptions: Option[DebugOptions] = None, - override val recentFollowedUserIds: Option[Seq[Long]], - override val recentFollowedByUserIds: Option[Seq[Long]], - override val wtfImpressions: Option[Seq[WtfImpression]], - override val similarToUserIds: Seq[Long], - candidates: Seq[CandidateUser], - debugParams: Option[DebugParams] = None, - isSoftUser: Boolean = false) - extends HasClientContext - with HasDisplayLocation - with HasParams - with HasDebugOptions - with HasPreFetchedFeature - with HasSimilarToContext { - def toOfflineThrift: offline.OfflineScoringUserRequest = offline.OfflineScoringUserRequest( - ClientContextConverter.toFRSOfflineClientContextThrift(clientContext), - displayLocation.toOfflineThrift, - candidates.map(_.toOfflineUserThrift) - ) - def toRecommendationRequest: RecommendationRequest = RecommendationRequest( - clientContext = clientContext, - displayLocation = displayLocation, - displayContext = None, - maxResults = None, - cursor = None, - excludedIds = None, - fetchPromotedContent = None, - debugParams = debugParams, - isSoftUser = isSoftUser - ) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/ScoringUserResponse.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/ScoringUserResponse.scala deleted file mode 100644 index 4611386d3..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/ScoringUserResponse.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.twitter.follow_recommendations.models - -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.logging.{thriftscala => offline} -import com.twitter.follow_recommendations.{thriftscala => t} - -case class ScoringUserResponse(candidates: Seq[CandidateUser]) { - lazy val toThrift: t.ScoringUserResponse = - t.ScoringUserResponse(candidates.map(_.toUserThrift)) - - lazy val toRecommendationResponse: RecommendationResponse = RecommendationResponse(candidates) - - lazy val toOfflineThrift: offline.OfflineScoringUserResponse = - offline.OfflineScoringUserResponse(candidates.map(_.toOfflineUserThrift)) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/failures/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/failures/BUILD deleted file mode 100644 index 4874d636e..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/failures/BUILD +++ /dev/null @@ -1,8 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/pipeline_failure", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/failures/TimeoutPipelineFailure.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/failures/TimeoutPipelineFailure.scala deleted file mode 100644 index 023b4c63e..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models/failures/TimeoutPipelineFailure.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.models.failures - -import com.twitter.product_mixer.core.pipeline.pipeline_failure.CandidateSourceTimeout -import com.twitter.product_mixer.core.pipeline.pipeline_failure.PipelineFailure - -object TimeoutPipelineFailure { - def apply(candidateSourceName: String): PipelineFailure = { - PipelineFailure( - CandidateSourceTimeout, - s"Candidate Source $candidateSourceName timed out before returning candidates") - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ABDeciderModule.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ABDeciderModule.scala deleted file mode 100644 index b75b6753e..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ABDeciderModule.scala +++ /dev/null @@ -1,31 +0,0 @@ -package com.twitter.follow_recommendations.modules - -import com.google.inject.Provides -import com.google.inject.name.Named -import com.twitter.abdecider.ABDeciderFactory -import com.twitter.abdecider.LoggingABDecider -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.inject.TwitterModule -import com.twitter.logging.LoggerFactory -import javax.inject.Singleton - -object ABDeciderModule extends TwitterModule { - @Provides - @Singleton - def provideABDecider( - stats: StatsReceiver, - @Named(GuiceNamedConstants.CLIENT_EVENT_LOGGER) factory: LoggerFactory - ): LoggingABDecider = { - - val ymlPath = "/usr/local/config/abdecider/abdecider.yml" - - val abDeciderFactory = ABDeciderFactory( - abDeciderYmlPath = ymlPath, - scribeLogger = Some(factory()), - environment = Some("production") - ) - - abDeciderFactory.buildWithLogging() - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/BUILD deleted file mode 100644 index e7fb68380..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/BUILD +++ /dev/null @@ -1,24 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/com/google/inject:guice", - "3rdparty/jvm/com/google/inject/extensions:guice-assistedinject", - "3rdparty/jvm/javax/inject:javax.inject", - "3rdparty/jvm/net/codingwell:scala-guice", - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra-internal/mtls-thriftmux/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/constants", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/transforms/modify_social_proof", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products", - "follow-recommendations-service/thrift/src/main/thrift:thrift-scala", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product/guice", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product/registry", - "twml/runtime/src/main/scala/com/twitter/deepbird/runtime/prediction_engine", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ConfigApiModule.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ConfigApiModule.scala deleted file mode 100644 index ef3865bf2..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ConfigApiModule.scala +++ /dev/null @@ -1,20 +0,0 @@ -package com.twitter.follow_recommendations.modules - -import com.google.inject.Provides -import com.twitter.decider.Decider -import com.twitter.follow_recommendations.configapi.ConfigBuilder -import com.twitter.inject.TwitterModule -import com.twitter.servo.decider.DeciderGateBuilder -import com.twitter.timelines.configapi.Config -import javax.inject.Singleton - -object ConfigApiModule extends TwitterModule { - @Provides - @Singleton - def providesDeciderGateBuilder(decider: Decider): DeciderGateBuilder = - new DeciderGateBuilder(decider) - - @Provides - @Singleton - def providesConfig(configBuilder: ConfigBuilder): Config = configBuilder.build() -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/DiffyModule.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/DiffyModule.scala deleted file mode 100644 index 4ab0e4eba..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/DiffyModule.scala +++ /dev/null @@ -1,71 +0,0 @@ -package com.twitter.follow_recommendations.modules - -import com.google.inject.Provides -import com.google.inject.Singleton -import com.twitter.inject.annotations.Flag -import com.twitter.decider.RandomRecipient -import com.twitter.finagle.ThriftMux -import com.twitter.finagle.mtls.authentication.ServiceIdentifier -import com.twitter.finagle.mtls.client.MtlsStackClient.MtlsThriftMuxClientSyntax -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.finagle.thrift.ClientId -import com.twitter.finatra.annotations.DarkTrafficService -import com.twitter.follow_recommendations.configapi.deciders.DeciderKey -import com.twitter.follow_recommendations.thriftscala.FollowRecommendationsThriftService -import com.twitter.inject.TwitterModule -import com.twitter.inject.thrift.filters.DarkTrafficFilter -import com.twitter.servo.decider.DeciderGateBuilder - -object DiffyModule extends TwitterModule { - // diffy.dest is defined in the Follow Recommendations Service aurora file - // and points to the Dark Traffic Proxy server - private val destFlag = - flag[String]("diffy.dest", "/$/nil", "Resolvable name of diffy-service or proxy") - - @Provides - @Singleton - @DarkTrafficService - def provideDarkTrafficService( - serviceIdentifier: ServiceIdentifier - ): FollowRecommendationsThriftService.ReqRepServicePerEndpoint = { - ThriftMux.client - .withClientId(ClientId("follow_recos_service_darktraffic_proxy_client")) - .withMutualTls(serviceIdentifier) - .servicePerEndpoint[FollowRecommendationsThriftService.ReqRepServicePerEndpoint]( - dest = destFlag(), - label = "darktrafficproxy" - ) - } - - @Provides - @Singleton - def provideDarkTrafficFilter( - @DarkTrafficService darkService: FollowRecommendationsThriftService.ReqRepServicePerEndpoint, - deciderGateBuilder: DeciderGateBuilder, - statsReceiver: StatsReceiver, - @Flag("environment") env: String - ): DarkTrafficFilter[FollowRecommendationsThriftService.ReqRepServicePerEndpoint] = { - // sampleFunction is used to determine which requests should get replicated - // to the dark traffic proxy server - val sampleFunction: Any => Boolean = { _ => - // check whether the current FRS instance is deployed in production - env match { - case "prod" => - statsReceiver.scope("provideDarkTrafficFilter").counter("prod").incr() - destFlag.isDefined && deciderGateBuilder - .keyToFeature(DeciderKey.EnableTrafficDarkReading).isAvailable(RandomRecipient) - case _ => - statsReceiver.scope("provideDarkTrafficFilter").counter("devel").incr() - // replicate zero requests if in non-production environment - false - } - } - new DarkTrafficFilter[FollowRecommendationsThriftService.ReqRepServicePerEndpoint]( - darkService, - sampleFunction, - forwardAfterService = true, - statsReceiver.scope("DarkTrafficFilter"), - lookupByMethod = true - ) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/FeatureSwitchesModule.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/FeatureSwitchesModule.scala deleted file mode 100644 index 1600344b6..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/FeatureSwitchesModule.scala +++ /dev/null @@ -1,85 +0,0 @@ -package com.twitter.follow_recommendations.modules - -import com.google.inject.Provides -import com.twitter.abdecider.LoggingABDecider -import com.twitter.featureswitches.v2.Feature -import com.twitter.featureswitches.v2.FeatureFilter -import com.twitter.featureswitches.v2.FeatureSwitches -import com.twitter.featureswitches.v2.builder.FeatureSwitchesBuilder -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants.PRODUCER_SIDE_FEATURE_SWITCHES -import com.twitter.inject.TwitterModule -import javax.inject.Named -import javax.inject.Singleton - -object FeaturesSwitchesModule extends TwitterModule { - private val DefaultConfigRepoPath = "/usr/local/config" - private val FeaturesPath = "/features/onboarding/follow-recommendations-service/main" - val isLocal = flag("configrepo.local", false, "Is the server running locally or in a DC") - val localConfigRepoPath = flag( - "local.configrepo", - System.getProperty("user.home") + "/workspace/config", - "Path to your local config repo" - ) - - @Provides - @Singleton - def providesFeatureSwitches( - abDecider: LoggingABDecider, - statsReceiver: StatsReceiver - ): FeatureSwitches = { - val configRepoPath = if (isLocal()) { - localConfigRepoPath() - } else { - DefaultConfigRepoPath - } - - FeatureSwitchesBuilder - .createDefault(FeaturesPath, abDecider, Some(statsReceiver)) - .configRepoAbsPath(configRepoPath) - .serviceDetailsFromAurora() - .build() - } - - @Provides - @Singleton - @Named(PRODUCER_SIDE_FEATURE_SWITCHES) - def providesProducerFeatureSwitches( - abDecider: LoggingABDecider, - statsReceiver: StatsReceiver - ): FeatureSwitches = { - val configRepoPath = if (isLocal()) { - localConfigRepoPath() - } else { - DefaultConfigRepoPath - } - - /** - * Feature Switches evaluate all tied FS Keys on Params construction time, which is very inefficient - * for producer/candidate side holdbacks because we have 100s of candidates, and 100s of FS which result - * in 10,000 FS evaluations when we want 1 per candidate (100 total), so we create a new FS Client - * which has a [[ProducerFeatureFilter]] set for feature filter to reduce the FS Keys we evaluate. - */ - FeatureSwitchesBuilder - .createDefault(FeaturesPath, abDecider, Some(statsReceiver.scope("producer_side_fs"))) - .configRepoAbsPath(configRepoPath) - .serviceDetailsFromAurora() - .addFeatureFilter(ProducerFeatureFilter) - .build() - } -} - -case object ProducerFeatureFilter extends FeatureFilter { - private val AllowedKeys = Set( - "post_nux_ml_flow_candidate_user_scorer_id", - "frs_receiver_holdback_keep_social_user_candidate", - "frs_receiver_holdback_keep_user_candidate") - - override def filter(feature: Feature): Option[Feature] = { - if (AllowedKeys.exists(feature.parameters.contains)) { - Some(feature) - } else { - None - } - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/FlagsModule.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/FlagsModule.scala deleted file mode 100644 index f8ff5ae94..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/FlagsModule.scala +++ /dev/null @@ -1,18 +0,0 @@ -package com.twitter.follow_recommendations.modules -import com.twitter.inject.TwitterModule - -object FlagsModule extends TwitterModule { - flag[Boolean]( - name = "fetch_prod_promoted_accounts", - help = "Whether or not to fetch production promoted accounts (true / false)" - ) - flag[Boolean]( - name = "interests_opt_out_prod_enabled", - help = "Whether to fetch intersts opt out data from the prod strato column or not" - ) - flag[Boolean]( - name = "log_results", - default = false, - help = "Whether to log results such that we use them for scoring or metrics" - ) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ProductRegistryModule.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ProductRegistryModule.scala deleted file mode 100644 index 218f3b973..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ProductRegistryModule.scala +++ /dev/null @@ -1,12 +0,0 @@ -package com.twitter.follow_recommendations.modules - -import com.twitter.follow_recommendations.products.ProdProductRegistry -import com.twitter.follow_recommendations.products.common.ProductRegistry -import com.twitter.inject.TwitterModule -import javax.inject.Singleton - -object ProductRegistryModule extends TwitterModule { - override protected def configure(): Unit = { - bind[ProductRegistry].to[ProdProductRegistry].in[Singleton] - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ScorerModule.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ScorerModule.scala deleted file mode 100644 index 035cc04bf..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ScorerModule.scala +++ /dev/null @@ -1,40 +0,0 @@ -package com.twitter.follow_recommendations.modules - -import com.google.inject.Provides -import com.google.inject.Singleton -import com.twitter.inject.TwitterModule -import com.twitter.relevance.ep_model.common.CommonConstants -import com.twitter.relevance.ep_model.scorer.EPScorer -import com.twitter.relevance.ep_model.scorer.EPScorerBuilder -import java.io.File -import java.io.FileOutputStream -import scala.language.postfixOps - -object ScorerModule extends TwitterModule { - private val STPScorerPath = "/quality/stp_models/20141223" - - private def fileFromResource(resource: String): File = { - val inputStream = getClass.getResourceAsStream(resource) - val file = File.createTempFile(resource, "temp") - val fos = new FileOutputStream(file) - Iterator - .continually(inputStream.read) - .takeWhile(-1 !=) - .foreach(fos.write) - file - } - - @Provides - @Singleton - def provideEpScorer: EPScorer = { - val modelPath = - fileFromResource(STPScorerPath + "/" + CommonConstants.EP_MODEL_FILE_NAME).getAbsolutePath - val trainingConfigPath = - fileFromResource(STPScorerPath + "/" + CommonConstants.TRAINING_CONFIG).getAbsolutePath - val epScorer = new EPScorerBuilder - epScorer - .withModelPath(modelPath) - .withTrainingConfig(trainingConfigPath) - .build() - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ScribeModule.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ScribeModule.scala deleted file mode 100644 index 35af77c1a..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/ScribeModule.scala +++ /dev/null @@ -1,95 +0,0 @@ -package com.twitter.follow_recommendations.modules - -import com.google.inject.Provides -import com.google.inject.Singleton -import com.google.inject.name.Named -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.constants.GuiceNamedConstants -import com.twitter.inject.TwitterModule -import com.twitter.logging.BareFormatter -import com.twitter.logging.HandlerFactory -import com.twitter.logging.Level -import com.twitter.logging.LoggerFactory -import com.twitter.logging.NullHandler -import com.twitter.logging.QueueingHandler -import com.twitter.logging.ScribeHandler - -object ScribeModule extends TwitterModule { - val useProdLogger = flag( - name = "scribe.use_prod_loggers", - default = false, - help = "whether to use production logging for service" - ) - - @Provides - @Singleton - @Named(GuiceNamedConstants.CLIENT_EVENT_LOGGER) - def provideClientEventsLoggerFactory(stats: StatsReceiver): LoggerFactory = { - val loggerCategory = "client_event" - val clientEventsHandler: HandlerFactory = if (useProdLogger()) { - QueueingHandler( - maxQueueSize = 10000, - handler = ScribeHandler( - category = loggerCategory, - formatter = BareFormatter, - level = Some(Level.INFO), - statsReceiver = stats.scope("client_event_scribe") - ) - ) - } else { () => NullHandler } - LoggerFactory( - node = "abdecider", - level = Some(Level.INFO), - useParents = false, - handlers = clientEventsHandler :: Nil - ) - } - - @Provides - @Singleton - @Named(GuiceNamedConstants.REQUEST_LOGGER) - def provideFollowRecommendationsLoggerFactory(stats: StatsReceiver): LoggerFactory = { - val loggerCategory = "follow_recommendations_logs" - val handlerFactory: HandlerFactory = if (useProdLogger()) { - QueueingHandler( - maxQueueSize = 10000, - handler = ScribeHandler( - category = loggerCategory, - formatter = BareFormatter, - level = Some(Level.INFO), - statsReceiver = stats.scope("follow_recommendations_logs_scribe") - ) - ) - } else { () => NullHandler } - LoggerFactory( - node = loggerCategory, - level = Some(Level.INFO), - useParents = false, - handlers = handlerFactory :: Nil - ) - } - - @Provides - @Singleton - @Named(GuiceNamedConstants.FLOW_LOGGER) - def provideFrsRecommendationFlowLoggerFactory(stats: StatsReceiver): LoggerFactory = { - val loggerCategory = "frs_recommendation_flow_logs" - val handlerFactory: HandlerFactory = if (useProdLogger()) { - QueueingHandler( - maxQueueSize = 10000, - handler = ScribeHandler( - category = loggerCategory, - formatter = BareFormatter, - level = Some(Level.INFO), - statsReceiver = stats.scope("frs_recommendation_flow_logs_scribe") - ) - ) - } else { () => NullHandler } - LoggerFactory( - node = loggerCategory, - level = Some(Level.INFO), - useParents = false, - handlers = handlerFactory :: Nil - ) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/TimerModule.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/TimerModule.scala deleted file mode 100644 index 0572e43bf..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/modules/TimerModule.scala +++ /dev/null @@ -1,13 +0,0 @@ -package com.twitter.follow_recommendations.modules - -import com.google.inject.Provides -import com.google.inject.Singleton -import com.twitter.finagle.memcached.ZookeeperStateMonitor.DefaultTimer -import com.twitter.inject.TwitterModule -import com.twitter.util.Timer - -object TimerModule extends TwitterModule { - @Provides - @Singleton - def providesTimer: Timer = DefaultTimer -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/BUILD deleted file mode 100644 index 5840c0f2f..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/BUILD +++ /dev/null @@ -1,16 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "3rdparty/jvm/org/slf4j:slf4j-api", - "finatra/inject/inject-app/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product/guice", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/ProdProductRegistry.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/ProdProductRegistry.scala deleted file mode 100644 index 9a0dbb995..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/ProdProductRegistry.scala +++ /dev/null @@ -1,44 +0,0 @@ -package com.twitter.follow_recommendations.products - -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.products.common.ProductRegistry -import com.twitter.follow_recommendations.products.explore_tab.ExploreTabProduct -import com.twitter.follow_recommendations.products.home_timeline.HomeTimelineProduct -import com.twitter.follow_recommendations.products.home_timeline_tweet_recs.HomeTimelineTweetRecsProduct -import com.twitter.follow_recommendations.products.sidebar.SidebarProduct - -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ProdProductRegistry @Inject() ( - exploreTabProduct: ExploreTabProduct, - homeTimelineProduct: HomeTimelineProduct, - homeTimelineTweetRecsProduct: HomeTimelineTweetRecsProduct, - sidebarProduct: SidebarProduct, -) extends ProductRegistry { - - override val products: Seq[common.Product] = - Seq( - exploreTabProduct, - homeTimelineProduct, - homeTimelineTweetRecsProduct, - sidebarProduct - ) - - override val displayLocationProductMap: Map[DisplayLocation, common.Product] = - products.groupBy(_.displayLocation).flatMap { - case (loc, products) => - assert(products.size == 1, s"Found more than 1 Product for ${loc}") - products.headOption.map { product => loc -> product } - } - - override def getProductByDisplayLocation(displayLocation: DisplayLocation): common.Product = { - displayLocationProductMap.getOrElse( - displayLocation, - throw new MissingProductException(displayLocation)) - } -} - -class MissingProductException(displayLocation: DisplayLocation) - extends Exception(s"No Product found for ${displayLocation}") diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/BUILD deleted file mode 100644 index 4b32816e4..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/BUILD +++ /dev/null @@ -1,12 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "configapi/configapi-core", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models", - "follow-recommendations-service/thrift/src/main/thrift:thrift-scala", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/Exceptions.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/Exceptions.scala deleted file mode 100644 index c00d8c407..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/Exceptions.scala +++ /dev/null @@ -1,7 +0,0 @@ -package com.twitter.follow_recommendations.products.common - -abstract class ProductException(message: String) extends Exception(message) - -class MissingFieldException(productRequest: ProductRequest, fieldName: String) - extends ProductException( - s"Missing ${fieldName} field for ${productRequest.recommendationRequest.displayLocation} request") diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/Product.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/Product.scala deleted file mode 100644 index 28c348204..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/Product.scala +++ /dev/null @@ -1,56 +0,0 @@ -package com.twitter.follow_recommendations.products.common - -import com.twitter.follow_recommendations.assembler.models.Layout -import com.twitter.follow_recommendations.common.base.BaseRecommendationFlow -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.common.models.Recommendation -import com.twitter.follow_recommendations.models.RecommendationRequest -import com.twitter.product_mixer.core.model.marshalling.request.{Product => ProductMixerProduct} -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.Params - -trait Product { - - /** Each product also requires a human-readable name. - * You can change this at any time - */ - def name: String - - /** - * Every product needs a machine-friendly identifier for internal use. - * You should use the same name as the product package name. - * Except dashes are better than underscore - * - * Avoid changing this once it's in production. - */ - def identifier: String - - def displayLocation: DisplayLocation - - def selectWorkflows( - request: ProductRequest - ): Stitch[Seq[BaseRecommendationFlow[ProductRequest, _ <: Recommendation]]] - - /** - * Blender is responsible for blending together the candidates generated by different flows used - * in a product. For example, if a product uses two flows, it is blender's responsibility to - * interleave their generated candidates together and make a unified sequence of candidates. - */ - def blender: Transform[ProductRequest, Recommendation] - - /** - * It is resultsTransformer job to do any final transformations needed on the final list of - * candidates generated by a product. For example, if a final quality check on candidates needed, - * resultsTransformer will handle it. - */ - def resultsTransformer(request: ProductRequest): Stitch[Transform[ProductRequest, Recommendation]] - - def enabled(request: ProductRequest): Stitch[Boolean] - - def layout: Option[Layout] = None - - def productMixerProduct: Option[ProductMixerProduct] = None -} - -case class ProductRequest(recommendationRequest: RecommendationRequest, params: Params) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/ProductRegistry.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/ProductRegistry.scala deleted file mode 100644 index fbe486536..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common/ProductRegistry.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.products.common - -import com.twitter.follow_recommendations.common.models.DisplayLocation - -trait ProductRegistry { - def products: Seq[Product] - def displayLocationProductMap: Map[DisplayLocation, Product] - def getProductByDisplayLocation(displayLocation: DisplayLocation): Product -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/BUILD deleted file mode 100644 index 2f9412612..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "finatra/inject/inject-app/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/ExploreTabProduct.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/ExploreTabProduct.scala deleted file mode 100644 index a49fccb45..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/ExploreTabProduct.scala +++ /dev/null @@ -1,50 +0,0 @@ -package com.twitter.follow_recommendations.products.explore_tab - -import com.twitter.follow_recommendations.common.base.BaseRecommendationFlow -import com.twitter.follow_recommendations.common.base.IdentityTransform -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.common.models.Recommendation -import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlFlow -import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlRequestBuilder -import com.twitter.follow_recommendations.products.common.Product -import com.twitter.follow_recommendations.products.common.ProductRequest -import com.twitter.follow_recommendations.products.explore_tab.configapi.ExploreTabParams -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ExploreTabProduct @Inject() ( - postNuxMlFlow: PostNuxMlFlow, - postNuxMlRequestBuilder: PostNuxMlRequestBuilder) - extends Product { - override val name: String = "Explore Tab" - - override val identifier: String = "explore-tab" - - override val displayLocation: DisplayLocation = DisplayLocation.ExploreTab - - override def selectWorkflows( - request: ProductRequest - ): Stitch[Seq[BaseRecommendationFlow[ProductRequest, _ <: Recommendation]]] = { - postNuxMlRequestBuilder.build(request).map { postNuxMlRequest => - Seq(postNuxMlFlow.mapKey({ _: ProductRequest => postNuxMlRequest })) - } - } - - override val blender: Transform[ProductRequest, Recommendation] = - new IdentityTransform[ProductRequest, Recommendation] - - override def resultsTransformer( - request: ProductRequest - ): Stitch[Transform[ProductRequest, Recommendation]] = - Stitch.value(new IdentityTransform[ProductRequest, Recommendation]) - - override def enabled(request: ProductRequest): Stitch[Boolean] = { - // Ideally we should hook up is_soft_user as custom FS field and disable the product through FS - val enabledForUserType = !request.recommendationRequest.isSoftUser || request.params( - ExploreTabParams.EnableProductForSoftUser) - Stitch.value(request.params(ExploreTabParams.EnableProduct) && enabledForUserType) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/BUILD deleted file mode 100644 index 3bb732e35..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "configapi/configapi-core", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/ExploreTabFSConfig.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/ExploreTabFSConfig.scala deleted file mode 100644 index 092252aca..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/ExploreTabFSConfig.scala +++ /dev/null @@ -1,14 +0,0 @@ -package com.twitter.follow_recommendations.products.explore_tab.configapi - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.follow_recommendations.products.explore_tab.configapi.ExploreTabParams._ -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.Param -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ExploreTabFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = - Seq(EnableProductForSoftUser) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/ExploreTabParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/ExploreTabParams.scala deleted file mode 100644 index b9d9d3b87..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/explore_tab/configapi/ExploreTabParams.scala +++ /dev/null @@ -1,10 +0,0 @@ -package com.twitter.follow_recommendations.products.explore_tab.configapi - -import com.twitter.timelines.configapi.Param -import com.twitter.timelines.configapi.FSParam - -object ExploreTabParams { - object EnableProduct extends Param[Boolean](false) - object EnableProductForSoftUser - extends FSParam[Boolean]("explore_tab_enable_product_for_soft_user", false) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/BUILD deleted file mode 100644 index 4b0586ff7..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "finatra/inject/inject-app/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HTLProductMixer.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HTLProductMixer.scala deleted file mode 100644 index a2051c150..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HTLProductMixer.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.products.home_timeline - -import com.twitter.product_mixer.core.model.common.identifier.ProductIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.Product - -case object HTLProductMixer extends Product { - override val identifier: ProductIdentifier = ProductIdentifier("HomeTimeline") - override val stringCenterProject: Option[String] = Some("people-discovery") -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HomeTimelineProduct.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HomeTimelineProduct.scala deleted file mode 100644 index 590aab182..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HomeTimelineProduct.scala +++ /dev/null @@ -1,114 +0,0 @@ -package com.twitter.follow_recommendations.products.home_timeline - -import com.twitter.follow_recommendations.assembler.models.ActionConfig -import com.twitter.follow_recommendations.assembler.models.FollowedByUsersProof -import com.twitter.follow_recommendations.assembler.models.FooterConfig -import com.twitter.follow_recommendations.assembler.models.GeoContextProof -import com.twitter.follow_recommendations.assembler.models.HeaderConfig -import com.twitter.follow_recommendations.assembler.models.Layout -import com.twitter.follow_recommendations.assembler.models.TitleConfig -import com.twitter.follow_recommendations.assembler.models.UserListLayout -import com.twitter.follow_recommendations.assembler.models.UserListOptions -import com.twitter.follow_recommendations.common.base.BaseRecommendationFlow -import com.twitter.follow_recommendations.common.base.IdentityTransform -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.follow_recommendations.flows.ads.PromotedAccountsFlow -import com.twitter.follow_recommendations.flows.ads.PromotedAccountsFlowRequest -import com.twitter.follow_recommendations.blenders.PromotedAccountsBlender -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.common.models.Recommendation -import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlFlow -import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlRequestBuilder -import com.twitter.follow_recommendations.products.common.Product -import com.twitter.follow_recommendations.products.common.ProductRequest -import com.twitter.follow_recommendations.products.home_timeline.configapi.HomeTimelineParams._ -import com.twitter.inject.Injector -import com.twitter.product_mixer.core.model.marshalling.request -import com.twitter.product_mixer.core.product.guice.ProductScope -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class HomeTimelineProduct @Inject() ( - postNuxMlFlow: PostNuxMlFlow, - postNuxMlRequestBuilder: PostNuxMlRequestBuilder, - promotedAccountsFlow: PromotedAccountsFlow, - promotedAccountsBlender: PromotedAccountsBlender, - productScope: ProductScope, - injector: Injector, -) extends Product { - - override val name: String = "Home Timeline" - - override val identifier: String = "home-timeline" - - override val displayLocation: DisplayLocation = DisplayLocation.HomeTimeline - - override def selectWorkflows( - request: ProductRequest - ): Stitch[Seq[BaseRecommendationFlow[ProductRequest, _ <: Recommendation]]] = { - postNuxMlRequestBuilder.build(request).map { postNuxMlRequest => - Seq( - postNuxMlFlow.mapKey({ request: ProductRequest => postNuxMlRequest }), - promotedAccountsFlow.mapKey(mkPromotedAccountsRequest)) - } - } - - override val blender: Transform[ProductRequest, Recommendation] = { - promotedAccountsBlender.mapTarget[ProductRequest](getMaxResults) - } - - private val identityTransform = new IdentityTransform[ProductRequest, Recommendation] - - override def resultsTransformer( - request: ProductRequest - ): Stitch[Transform[ProductRequest, Recommendation]] = Stitch.value(identityTransform) - - override def enabled(request: ProductRequest): Stitch[Boolean] = - Stitch.value(request.params(EnableProduct)) - - override def layout: Option[Layout] = { - productMixerProduct.map { product => - val homeTimelineStrings = productScope.let(product) { - injector.instance[HomeTimelineStrings] - } - UserListLayout( - header = Some(HeaderConfig(TitleConfig(homeTimelineStrings.whoToFollowModuleTitle))), - userListOptions = UserListOptions(userBioEnabled = true, userBioTruncated = true, None), - socialProofs = Some( - Seq( - FollowedByUsersProof( - homeTimelineStrings.whoToFollowFollowedByManyUserSingleString, - homeTimelineStrings.whoToFollowFollowedByManyUserDoubleString, - homeTimelineStrings.whoToFollowFollowedByManyUserMultipleString - ), - GeoContextProof(homeTimelineStrings.whoToFollowPopularInCountryKey) - )), - footer = Some( - FooterConfig( - Some(ActionConfig(homeTimelineStrings.whoToFollowModuleFooter, "http://twitter.com")))) - ) - } - } - - override def productMixerProduct: Option[request.Product] = Some(HTLProductMixer) - - private[home_timeline] def mkPromotedAccountsRequest( - req: ProductRequest - ): PromotedAccountsFlowRequest = { - PromotedAccountsFlowRequest( - req.recommendationRequest.clientContext, - req.params, - req.recommendationRequest.displayLocation, - None, - req.recommendationRequest.excludedIds.getOrElse(Nil) - ) - } - - private[home_timeline] def getMaxResults(req: ProductRequest): Int = { - req.recommendationRequest.maxResults.getOrElse( - req.params(DefaultMaxResults) - ) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HomeTimelineStrings.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HomeTimelineStrings.scala deleted file mode 100644 index 75819555e..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/HomeTimelineStrings.scala +++ /dev/null @@ -1,26 +0,0 @@ -package com.twitter.follow_recommendations.products.home_timeline - -import com.twitter.product_mixer.core.product.guice.scope.ProductScoped -import com.twitter.stringcenter.client.ExternalStringRegistry -import com.twitter.stringcenter.client.core.ExternalString -import javax.inject.Inject -import javax.inject.Provider -import javax.inject.Singleton - -@Singleton -class HomeTimelineStrings @Inject() ( - @ProductScoped externalStringRegistryProvider: Provider[ExternalStringRegistry]) { - private val externalStringRegistry = externalStringRegistryProvider.get() - val whoToFollowFollowedByManyUserSingleString: ExternalString = - externalStringRegistry.createProdString("WtfRecommendationContext.followedByManyUserSingle") - val whoToFollowFollowedByManyUserDoubleString: ExternalString = - externalStringRegistry.createProdString("WtfRecommendationContext.followedByManyUserDouble") - val whoToFollowFollowedByManyUserMultipleString: ExternalString = - externalStringRegistry.createProdString("WtfRecommendationContext.followedByManyUserMultiple") - val whoToFollowPopularInCountryKey: ExternalString = - externalStringRegistry.createProdString("WtfRecommendationContext.popularInCountry") - val whoToFollowModuleTitle: ExternalString = - externalStringRegistry.createProdString("WhoToFollowModule.title") - val whoToFollowModuleFooter: ExternalString = - externalStringRegistry.createProdString("WhoToFollowModule.pivot") -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/BUILD deleted file mode 100644 index 3bb732e35..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/BUILD +++ /dev/null @@ -1,9 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "configapi/configapi-core", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/common", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/HomeTimelineFSConfig.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/HomeTimelineFSConfig.scala deleted file mode 100644 index 15e97b3a5..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/HomeTimelineFSConfig.scala +++ /dev/null @@ -1,22 +0,0 @@ -package com.twitter.follow_recommendations.products.home_timeline.configapi - -import com.twitter.follow_recommendations.configapi.common.FeatureSwitchConfig -import com.twitter.follow_recommendations.products.home_timeline.configapi.HomeTimelineParams._ -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSName -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.timelines.configapi.Param -import com.twitter.util.Duration -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class HomeTimelineFSConfig @Inject() () extends FeatureSwitchConfig { - override val booleanFSParams: Seq[Param[Boolean] with FSName] = - Seq(EnableWritingServingHistory) - - override val durationFSParams: Seq[FSBoundedParam[Duration] with HasDurationConversion] = Seq( - DurationGuardrailToForceSuggest, - SuggestBasedFatigueDuration - ) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/HomeTimelineParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/HomeTimelineParams.scala deleted file mode 100644 index 65ab5ae23..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline/configapi/HomeTimelineParams.scala +++ /dev/null @@ -1,38 +0,0 @@ -package com.twitter.follow_recommendations.products.home_timeline.configapi - -import com.twitter.conversions.DurationOps._ -import com.twitter.timelines.configapi.DurationConversion -import com.twitter.timelines.configapi.FSBoundedParam -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.HasDurationConversion -import com.twitter.timelines.configapi.Param -import com.twitter.util.Duration - -object HomeTimelineParams { - object EnableProduct extends Param[Boolean](false) - - object DefaultMaxResults extends Param[Int](20) - - object EnableWritingServingHistory - extends FSParam[Boolean]("home_timeline_enable_writing_serving_history", false) - - object DurationGuardrailToForceSuggest - extends FSBoundedParam[Duration]( - name = "home_timeline_duration_guardrail_to_force_suggest_in_hours", - default = 0.hours, - min = 0.hours, - max = 1000.hours) - with HasDurationConversion { - override val durationConversion: DurationConversion = DurationConversion.FromHours - } - - object SuggestBasedFatigueDuration - extends FSBoundedParam[Duration]( - name = "home_timeline_suggest_based_fatigue_duration_in_hours", - default = 0.hours, - min = 0.hours, - max = 1000.hours) - with HasDurationConversion { - override val durationConversion: DurationConversion = DurationConversion.FromHours - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/BUILD deleted file mode 100644 index 140cc928d..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/BUILD +++ /dev/null @@ -1,13 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "finatra/inject/inject-app/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/content_recommender_flow", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/configapi", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/HomeTimelineTweetRecsProduct.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/HomeTimelineTweetRecsProduct.scala deleted file mode 100644 index a5586f296..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/HomeTimelineTweetRecsProduct.scala +++ /dev/null @@ -1,50 +0,0 @@ -package com.twitter.follow_recommendations.products.home_timeline_tweet_recs - -import com.twitter.follow_recommendations.common.base.BaseRecommendationFlow -import com.twitter.follow_recommendations.common.base.IdentityTransform -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.common.models.Recommendation -import com.twitter.follow_recommendations.flows.content_recommender_flow.ContentRecommenderFlow -import com.twitter.follow_recommendations.flows.content_recommender_flow.ContentRecommenderRequestBuilder -import com.twitter.follow_recommendations.products.common.Product -import com.twitter.follow_recommendations.products.common.ProductRequest -import com.twitter.follow_recommendations.products.home_timeline_tweet_recs.configapi.HomeTimelineTweetRecsParams._ -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -/* - * This "DisplayLocation" is used to generate user recommendations using the ContentRecommenderFlow. These recommendations are later used downstream - * to generate recommended tweets on Home Timeline. - */ -@Singleton -class HomeTimelineTweetRecsProduct @Inject() ( - contentRecommenderFlow: ContentRecommenderFlow, - contentRecommenderRequestBuilder: ContentRecommenderRequestBuilder) - extends Product { - override val name: String = "Home Timeline Tweet Recs" - - override val identifier: String = "home-timeline-tweet-recs" - - override val displayLocation: DisplayLocation = DisplayLocation.HomeTimelineTweetRecs - - override def selectWorkflows( - request: ProductRequest - ): Stitch[Seq[BaseRecommendationFlow[ProductRequest, _ <: Recommendation]]] = { - contentRecommenderRequestBuilder.build(request).map { contentRecommenderRequest => - Seq(contentRecommenderFlow.mapKey({ request: ProductRequest => contentRecommenderRequest })) - } - } - - override val blender: Transform[ProductRequest, Recommendation] = - new IdentityTransform[ProductRequest, Recommendation] - - override def resultsTransformer( - request: ProductRequest - ): Stitch[Transform[ProductRequest, Recommendation]] = - Stitch.value(new IdentityTransform[ProductRequest, Recommendation]) - - override def enabled(request: ProductRequest): Stitch[Boolean] = - Stitch.value(request.params(EnableProduct)) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/configapi/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/configapi/BUILD deleted file mode 100644 index 9be8d9647..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/configapi/BUILD +++ /dev/null @@ -1,10 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "configapi/configapi-core", - "configapi/configapi-decider", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/configapi/HomeTimelineTweetRecsParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/configapi/HomeTimelineTweetRecsParams.scala deleted file mode 100644 index 319a51847..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/home_timeline_tweet_recs/configapi/HomeTimelineTweetRecsParams.scala +++ /dev/null @@ -1,7 +0,0 @@ -package com.twitter.follow_recommendations.products.home_timeline_tweet_recs.configapi - -import com.twitter.timelines.configapi.Param - -object HomeTimelineTweetRecsParams { - object EnableProduct extends Param[Boolean](false) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/BUILD deleted file mode 100644 index f469a4748..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "finatra/inject/inject-app/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/blenders", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/ads", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/flows/post_nux_ml", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/common", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/configapi", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/SidebarProduct.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/SidebarProduct.scala deleted file mode 100644 index 29f788011..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/SidebarProduct.scala +++ /dev/null @@ -1,73 +0,0 @@ -package com.twitter.follow_recommendations.products.sidebar - -import com.twitter.follow_recommendations.common.base.BaseRecommendationFlow -import com.twitter.follow_recommendations.common.base.IdentityTransform -import com.twitter.follow_recommendations.common.base.Transform -import com.twitter.follow_recommendations.flows.ads.PromotedAccountsFlow -import com.twitter.follow_recommendations.flows.ads.PromotedAccountsFlowRequest -import com.twitter.follow_recommendations.blenders.PromotedAccountsBlender -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.follow_recommendations.common.models.Recommendation -import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlFlow -import com.twitter.follow_recommendations.flows.post_nux_ml.PostNuxMlRequestBuilder -import com.twitter.follow_recommendations.products.common.Product -import com.twitter.follow_recommendations.products.common.ProductRequest -import com.twitter.follow_recommendations.products.sidebar.configapi.SidebarParams -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class SidebarProduct @Inject() ( - postNuxMlFlow: PostNuxMlFlow, - postNuxMlRequestBuilder: PostNuxMlRequestBuilder, - promotedAccountsFlow: PromotedAccountsFlow, - promotedAccountsBlender: PromotedAccountsBlender) - extends Product { - override val name: String = "Sidebar" - - override val identifier: String = "sidebar" - - override val displayLocation: DisplayLocation = DisplayLocation.Sidebar - - override def selectWorkflows( - request: ProductRequest - ): Stitch[Seq[BaseRecommendationFlow[ProductRequest, _ <: Recommendation]]] = { - postNuxMlRequestBuilder.build(request).map { postNuxMlRequest => - Seq( - postNuxMlFlow.mapKey({ _: ProductRequest => postNuxMlRequest }), - promotedAccountsFlow.mapKey(mkPromotedAccountsRequest) - ) - } - } - - override val blender: Transform[ProductRequest, Recommendation] = { - promotedAccountsBlender.mapTarget[ProductRequest](getMaxResults) - } - - private[sidebar] def mkPromotedAccountsRequest( - req: ProductRequest - ): PromotedAccountsFlowRequest = { - PromotedAccountsFlowRequest( - req.recommendationRequest.clientContext, - req.params, - req.recommendationRequest.displayLocation, - None, - req.recommendationRequest.excludedIds.getOrElse(Nil) - ) - } - - private[sidebar] def getMaxResults(req: ProductRequest): Int = { - req.recommendationRequest.maxResults.getOrElse( - req.params(SidebarParams.DefaultMaxResults) - ) - } - - override def resultsTransformer( - request: ProductRequest - ): Stitch[Transform[ProductRequest, Recommendation]] = - Stitch.value(new IdentityTransform[ProductRequest, Recommendation]) - - override def enabled(request: ProductRequest): Stitch[Boolean] = - Stitch.value(request.params(SidebarParams.EnableProduct)) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/configapi/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/configapi/BUILD deleted file mode 100644 index 6fee24f89..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/configapi/BUILD +++ /dev/null @@ -1,8 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "configapi/configapi-core", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/configapi/SidebarParams.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/configapi/SidebarParams.scala deleted file mode 100644 index bbd026495..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products/sidebar/configapi/SidebarParams.scala +++ /dev/null @@ -1,9 +0,0 @@ -package com.twitter.follow_recommendations.products.sidebar.configapi - -import com.twitter.timelines.configapi.Param - -object SidebarParams { - object EnableProduct extends Param[Boolean](false) - - object DefaultMaxResults extends Param[Int](20) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/BUILD deleted file mode 100644 index fe29c22de..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/BUILD +++ /dev/null @@ -1,34 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - tags = ["bazel-compatible"], - dependencies = [ - "finatra/inject/inject-app/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "finatra/inject/inject-server/src/main/scala", - "finatra/inject/inject-thrift-client", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/crowd_search_accounts", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/real_graph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/top_organic_follows_accounts", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/clients/impression_store", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/predicates/sgs", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/ranking", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/rankers/ml_ranker/scoring", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/utils", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/assembler/models", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/deciders", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/logging", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/products", - "follow-recommendations-service/thrift/src/main/thrift:thrift-scala", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/pipeline/product", - "product-mixer/core/src/main/scala/com/twitter/product_mixer/core/product/registry", - "twitter-server/server/src/main/scala", - "util/util-app/src/main/scala", - "util/util-core:scala", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/FollowRecommendationsServiceWarmupHandler.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/FollowRecommendationsServiceWarmupHandler.scala deleted file mode 100644 index 7567fe9ce..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/FollowRecommendationsServiceWarmupHandler.scala +++ /dev/null @@ -1,101 +0,0 @@ -package com.twitter.follow_recommendations.services - -import com.twitter.finagle.thrift.ClientId -import com.twitter.finatra.thrift.routing.ThriftWarmup -import com.twitter.follow_recommendations.thriftscala.FollowRecommendationsThriftService.GetRecommendations -import com.twitter.follow_recommendations.thriftscala.ClientContext -import com.twitter.follow_recommendations.thriftscala.DebugParams -import com.twitter.follow_recommendations.thriftscala.DisplayContext -import com.twitter.follow_recommendations.thriftscala.DisplayLocation -import com.twitter.follow_recommendations.thriftscala.Profile -import com.twitter.follow_recommendations.thriftscala.RecommendationRequest -import com.twitter.inject.Logging -import com.twitter.inject.utils.Handler -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 -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class FollowRecommendationsServiceWarmupHandler @Inject() (warmup: ThriftWarmup) - extends Handler - with Logging { - - private val clientId = ClientId("thrift-warmup-client") - - override def handle(): Unit = { - val testIds = Seq(1L) - def warmupQuery(userId: Long, displayLocation: DisplayLocation): RecommendationRequest = { - val clientContext = ClientContext( - userId = Some(userId), - guestId = None, - appId = Some(258901L), - ipAddress = Some("0.0.0.0"), - userAgent = Some("FAKE_USER_AGENT_FOR_WARMUPS"), - countryCode = Some("US"), - languageCode = Some("en"), - isTwoffice = None, - userRoles = None, - deviceId = Some("FAKE_DEVICE_ID_FOR_WARMUPS") - ) - RecommendationRequest( - clientContext = clientContext, - displayLocation = displayLocation, - displayContext = None, - maxResults = Some(3), - fetchPromotedContent = Some(false), - debugParams = Some(DebugParams(doNotLog = Some(true))) - ) - } - - // Add FRS display locations here if they should be targeted for warm-up - // when FRS is starting from a fresh state after a deploy - val displayLocationsToWarmUp: Seq[DisplayLocation] = Seq( - DisplayLocation.HomeTimeline, - DisplayLocation.HomeTimelineReverseChron, - DisplayLocation.ProfileSidebar, - DisplayLocation.NuxInterests, - DisplayLocation.NuxPymk - ) - - try { - clientId.asCurrent { - // Iterate over each user ID created for testing - testIds foreach { id => - // Iterate over each display location targeted for warm-up - displayLocationsToWarmUp foreach { displayLocation => - val warmupReq = warmupQuery(id, displayLocation) - info(s"Sending warm-up request to service with query: $warmupReq") - warmup.sendRequest( - method = GetRecommendations, - req = Request(GetRecommendations.Args(warmupReq)))(assertWarmupResponse) - // send the request one more time so that it goes through cache hits - warmup.sendRequest( - method = GetRecommendations, - req = Request(GetRecommendations.Args(warmupReq)))(assertWarmupResponse) - } - } - } - } catch { - case e: Throwable => - // we don't want a warmup failure to prevent start-up - error(e.getMessage, e) - } - info("Warm-up done.") - } - - /* Private */ - - private def assertWarmupResponse(result: Try[Response[GetRecommendations.SuccessType]]): Unit = { - // we collect and log any exceptions from the result. - result match { - case Return(_) => // ok - case Throw(exception) => - warn() - error(s"Error performing warm-up request: ${exception.getMessage}", exception) - } - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductMixerRecommendationService.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductMixerRecommendationService.scala deleted file mode 100644 index daff9040d..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductMixerRecommendationService.scala +++ /dev/null @@ -1,72 +0,0 @@ -package com.twitter.follow_recommendations.services - -import com.twitter.finagle.stats.StatsReceiver -import javax.inject.Inject -import javax.inject.Singleton -import com.twitter.timelines.configapi.Params -import com.twitter.follow_recommendations.common.utils.DisplayLocationProductConverterUtil -import com.twitter.follow_recommendations.configapi.deciders.DeciderParams -import com.twitter.follow_recommendations.logging.FrsLogger -import com.twitter.follow_recommendations.models.{DebugParams => FrsDebugParams} -import com.twitter.follow_recommendations.models.RecommendationRequest -import com.twitter.follow_recommendations.models.RecommendationResponse -import com.twitter.follow_recommendations.models.Request -import com.twitter.product_mixer.core.model.marshalling.request.{ - DebugParams => ProductMixerDebugParams -} -import com.twitter.product_mixer.core.product.registry.ProductPipelineRegistry -import com.twitter.product_mixer.core.pipeline.product.ProductPipelineRequest -import com.twitter.stitch.Stitch - -@Singleton -class ProductMixerRecommendationService @Inject() ( - productPipelineRegistry: ProductPipelineRegistry, - resultLogger: FrsLogger, - baseStats: StatsReceiver) { - - private val stats = baseStats.scope("product_mixer_recos_service_stats") - private val loggingStats = stats.scope("logged") - - def get(request: RecommendationRequest, params: Params): Stitch[RecommendationResponse] = { - if (params(DeciderParams.EnableRecommendations)) { - val productMixerRequest = convertToProductMixerRequest(request) - - productPipelineRegistry - .getProductPipeline[Request, RecommendationResponse](productMixerRequest.product) - .process(ProductPipelineRequest(productMixerRequest, params)).onSuccess { response => - if (resultLogger.shouldLog(request.debugParams)) { - loggingStats.counter().incr() - resultLogger.logRecommendationResult(request, response) - } - } - } else { - Stitch.value(RecommendationResponse(Nil)) - } - - } - - def convertToProductMixerRequest(frsRequest: RecommendationRequest): Request = { - Request( - maxResults = frsRequest.maxResults, - debugParams = convertToProductMixerDebugParams(frsRequest.debugParams), - productContext = None, - product = - DisplayLocationProductConverterUtil.displayLocationToProduct(frsRequest.displayLocation), - clientContext = frsRequest.clientContext, - serializedRequestCursor = frsRequest.cursor, - frsDebugParams = frsRequest.debugParams, - displayLocation = frsRequest.displayLocation, - excludedIds = frsRequest.excludedIds, - fetchPromotedContent = frsRequest.fetchPromotedContent, - userLocationState = frsRequest.userLocationState - ) - } - - private def convertToProductMixerDebugParams( - frsDebugParams: Option[FrsDebugParams] - ): Option[ProductMixerDebugParams] = { - frsDebugParams.map { debugParams => - ProductMixerDebugParams(debugParams.featureOverrides, None) - } - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductPipelineSelector.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductPipelineSelector.scala deleted file mode 100644 index 1c949f03d..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductPipelineSelector.scala +++ /dev/null @@ -1,188 +0,0 @@ -package com.twitter.follow_recommendations.services - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.common.models.DebugOptions -import com.twitter.follow_recommendations.models.DebugParams -import com.twitter.follow_recommendations.models.RecommendationRequest -import com.twitter.follow_recommendations.models.RecommendationResponse -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.Params -import javax.inject.Inject -import javax.inject.Singleton -import scala.util.Random - -@Singleton -class ProductPipelineSelector @Inject() ( - recommendationsService: RecommendationsService, - productMixerRecommendationService: ProductMixerRecommendationService, - productPipelineSelectorConfig: ProductPipelineSelectorConfig, - baseStats: StatsReceiver) { - - private val frsStats = baseStats.scope("follow_recommendations_service") - private val stats = frsStats.scope("product_pipeline_selector_parity") - - private val readFromProductMixerCounter = stats.counter("select_product_mixer") - private val readFromOldFRSCounter = stats.counter("select_old_frs") - - def selectPipeline( - request: RecommendationRequest, - params: Params - ): Stitch[RecommendationResponse] = { - productPipelineSelectorConfig - .getDarkReadAndExpParams(request.displayLocation).map { darkReadAndExpParam => - if (params(darkReadAndExpParam.expParam)) { - readFromProductMixerPipeline(request, params) - } else if (params(darkReadAndExpParam.darkReadParam)) { - darkReadAndReturnResult(request, params) - } else { - readFromOldFrsPipeline(request, params) - } - }.getOrElse(readFromOldFrsPipeline(request, params)) - } - - private def readFromProductMixerPipeline( - request: RecommendationRequest, - params: Params - ): Stitch[RecommendationResponse] = { - readFromProductMixerCounter.incr() - productMixerRecommendationService.get(request, params) - } - - private def readFromOldFrsPipeline( - request: RecommendationRequest, - params: Params - ): Stitch[RecommendationResponse] = { - readFromOldFRSCounter.incr() - recommendationsService.get(request, params) - } - - private def darkReadAndReturnResult( - request: RecommendationRequest, - params: Params - ): Stitch[RecommendationResponse] = { - val darkReadStats = stats.scope("select_dark_read", request.displayLocation.toFsName) - darkReadStats.counter("count").incr() - - // If no seed is set, create a random one that both requests will use to remove differences - // in randomness for the WeightedCandidateSourceRanker - val randomizationSeed = new Random().nextLong() - - val oldFRSPiplelineRequest = request.copy( - debugParams = Some( - request.debugParams.getOrElse( - DebugParams(None, Some(DebugOptions(randomizationSeed = Some(randomizationSeed)))))) - ) - val productMixerPipelineRequest = request.copy( - debugParams = Some( - request.debugParams.getOrElse( - DebugParams( - None, - Some(DebugOptions(doNotLog = true, randomizationSeed = Some(randomizationSeed)))))) - ) - - StatsUtil - .profileStitch( - readFromOldFrsPipeline(oldFRSPiplelineRequest, params), - darkReadStats.scope("frs_timing")).applyEffect { frsOldPipelineResponse => - Stitch.async( - StatsUtil - .profileStitch( - readFromProductMixerPipeline(productMixerPipelineRequest, params), - darkReadStats.scope("product_mixer_timing")).liftToOption().map { - case Some(frsProductMixerResponse) => - darkReadStats.counter("product_mixer_pipeline_success").incr() - compare(request, frsOldPipelineResponse, frsProductMixerResponse) - case None => - darkReadStats.counter("product_mixer_pipeline_failure").incr() - } - ) - } - } - - def compare( - request: RecommendationRequest, - frsOldPipelineResponse: RecommendationResponse, - frsProductMixerResponse: RecommendationResponse - ): Unit = { - val compareStats = stats.scope("pipeline_comparison", request.displayLocation.toFsName) - compareStats.counter("total-comparisons").incr() - - val oldFrsMap = frsOldPipelineResponse.recommendations.map { user => user.id -> user }.toMap - val productMixerMap = frsProductMixerResponse.recommendations.map { user => - user.id -> user - }.toMap - - compareTopNResults(3, frsOldPipelineResponse, frsProductMixerResponse, compareStats) - compareTopNResults(5, frsOldPipelineResponse, frsProductMixerResponse, compareStats) - compareTopNResults(25, frsOldPipelineResponse, frsProductMixerResponse, compareStats) - compareTopNResults(50, frsOldPipelineResponse, frsProductMixerResponse, compareStats) - compareTopNResults(75, frsOldPipelineResponse, frsProductMixerResponse, compareStats) - - // Compare individual matching candidates - oldFrsMap.keys.foreach(userId => { - if (productMixerMap.contains(userId)) { - (oldFrsMap(userId), productMixerMap(userId)) match { - case (oldFrsUser: CandidateUser, productMixerUser: CandidateUser) => - compareStats.counter("matching-user-count").incr() - compareUser(oldFrsUser, productMixerUser, compareStats) - case _ => - compareStats.counter("unknown-user-type-count").incr() - } - } else { - compareStats.counter("missing-user-count").incr() - } - }) - } - - private def compareTopNResults( - n: Int, - frsOldPipelineResponse: RecommendationResponse, - frsProductMixerResponse: RecommendationResponse, - compareStats: StatsReceiver - ): Unit = { - if (frsOldPipelineResponse.recommendations.size >= n && frsProductMixerResponse.recommendations.size >= n) { - val oldFrsPipelineFirstN = frsOldPipelineResponse.recommendations.take(n).map(_.id) - val productMixerPipelineFirstN = frsProductMixerResponse.recommendations.take(n).map(_.id) - - if (oldFrsPipelineFirstN.sorted == productMixerPipelineFirstN.sorted) - compareStats.counter(s"first-$n-sorted-equal-ids").incr() - if (oldFrsPipelineFirstN == productMixerPipelineFirstN) - compareStats.counter(s"first-$n-unsorted-ids-equal").incr() - else - compareStats.counter(s"first-$n-unsorted-ids-unequal").incr() - } - } - - private def compareUser( - oldFrsUser: CandidateUser, - productMixerUser: CandidateUser, - stats: StatsReceiver - ): Unit = { - val userStats = stats.scope("matching-user") - - if (oldFrsUser.score != productMixerUser.score) - userStats.counter("mismatch-score").incr() - if (oldFrsUser.reason != productMixerUser.reason) - userStats.counter("mismatch-reason").incr() - if (oldFrsUser.userCandidateSourceDetails != productMixerUser.userCandidateSourceDetails) - userStats.counter("mismatch-userCandidateSourceDetails").incr() - if (oldFrsUser.adMetadata != productMixerUser.adMetadata) - userStats.counter("mismatch-adMetadata").incr() - if (oldFrsUser.trackingToken != productMixerUser.trackingToken) - userStats.counter("mismatch-trackingToken").incr() - if (oldFrsUser.dataRecord != productMixerUser.dataRecord) - userStats.counter("mismatch-dataRecord").incr() - if (oldFrsUser.scores != productMixerUser.scores) - userStats.counter("mismatch-scores").incr() - if (oldFrsUser.infoPerRankingStage != productMixerUser.infoPerRankingStage) - userStats.counter("mismatch-infoPerRankingStage").incr() - if (oldFrsUser.params != productMixerUser.params) - userStats.counter("mismatch-params").incr() - if (oldFrsUser.engagements != productMixerUser.engagements) - userStats.counter("mismatch-engagements").incr() - if (oldFrsUser.recommendationFlowIdentifier != productMixerUser.recommendationFlowIdentifier) - userStats.counter("mismatch-recommendationFlowIdentifier").incr() - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductPipelineSelectorConfig.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductPipelineSelectorConfig.scala deleted file mode 100644 index a1cac3316..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductPipelineSelectorConfig.scala +++ /dev/null @@ -1,19 +0,0 @@ -package com.twitter.follow_recommendations.services - -import com.twitter.follow_recommendations.common.models.DisplayLocation -import com.twitter.timelines.configapi.FSParam -import com.twitter.timelines.configapi.Param -import javax.inject.Singleton - -@Singleton -class ProductPipelineSelectorConfig { - private val paramsMap: Map[DisplayLocation, DarkReadAndExpParams] = Map.empty - - def getDarkReadAndExpParams( - displayLocation: DisplayLocation - ): Option[DarkReadAndExpParams] = { - paramsMap.get(displayLocation) - } -} - -case class DarkReadAndExpParams(darkReadParam: Param[Boolean], expParam: FSParam[Boolean]) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductRecommenderService.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductRecommenderService.scala deleted file mode 100644 index 967790a08..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/ProductRecommenderService.scala +++ /dev/null @@ -1,72 +0,0 @@ -package com.twitter.follow_recommendations.services - -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.StatsUtil -import com.twitter.follow_recommendations.common.models.Recommendation -import com.twitter.follow_recommendations.models.RecommendationRequest -import com.twitter.follow_recommendations.products.common.ProductRegistry -import com.twitter.follow_recommendations.products.common.ProductRequest -import com.twitter.stitch.Stitch -import com.twitter.follow_recommendations.configapi.params.GlobalParams.EnableWhoToFollowProducts -import com.twitter.timelines.configapi.Params -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class ProductRecommenderService @Inject() ( - productRegistry: ProductRegistry, - statsReceiver: StatsReceiver) { - - private val stats = statsReceiver.scope("ProductRecommenderService") - - def getRecommendations( - request: RecommendationRequest, - params: Params - ): Stitch[Seq[Recommendation]] = { - val displayLocation = request.displayLocation - val displayLocationStatName = displayLocation.toString - val locationStats = stats.scope(displayLocationStatName) - val loggedInOrOutStats = if (request.clientContext.userId.isDefined) { - stats.scope("logged_in").scope(displayLocationStatName) - } else { - stats.scope("logged_out").scope(displayLocationStatName) - } - - loggedInOrOutStats.counter("requests").incr() - val product = productRegistry.getProductByDisplayLocation(displayLocation) - val productRequest = ProductRequest(request, params) - val productEnabledStitch = - StatsUtil.profileStitch(product.enabled(productRequest), locationStats.scope("enabled")) - productEnabledStitch.flatMap { productEnabled => - if (productEnabled && params(EnableWhoToFollowProducts)) { - loggedInOrOutStats.counter("enabled").incr() - val stitch = for { - workflows <- StatsUtil.profileStitch( - product.selectWorkflows(productRequest), - locationStats.scope("select_workflows")) - workflowRecos <- StatsUtil.profileStitch( - Stitch.collect( - workflows.map(_.process(productRequest).map(_.result.getOrElse(Seq.empty)))), - locationStats.scope("execute_workflows") - ) - blendedCandidates <- StatsUtil.profileStitch( - product.blender.transform(productRequest, workflowRecos.flatten), - locationStats.scope("blend_results")) - resultsTransformer <- StatsUtil.profileStitch( - product.resultsTransformer(productRequest), - locationStats.scope("results_transformer")) - transformedCandidates <- StatsUtil.profileStitch( - resultsTransformer.transform(productRequest, blendedCandidates), - locationStats.scope("execute_results_transformer")) - } yield { - transformedCandidates - } - StatsUtil.profileStitchResults[Seq[Recommendation]](stitch, locationStats, _.size) - } else { - loggedInOrOutStats.counter("disabled").incr() - locationStats.counter("disabled_product").incr() - Stitch.Nil - } - } - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/RecommendationsService.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/RecommendationsService.scala deleted file mode 100644 index e4bc1e3c0..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/RecommendationsService.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.twitter.follow_recommendations.services - -import com.twitter.follow_recommendations.configapi.deciders.DeciderParams -import com.twitter.follow_recommendations.logging.FrsLogger -import com.twitter.follow_recommendations.models.RecommendationRequest -import com.twitter.follow_recommendations.models.RecommendationResponse -import com.twitter.stitch.Stitch -import com.twitter.timelines.configapi.Params -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class RecommendationsService @Inject() ( - productRecommenderService: ProductRecommenderService, - resultLogger: FrsLogger) { - def get(request: RecommendationRequest, params: Params): Stitch[RecommendationResponse] = { - if (params(DeciderParams.EnableRecommendations)) { - productRecommenderService - .getRecommendations(request, params).map(RecommendationResponse).onSuccess { response => - if (resultLogger.shouldLog(request.debugParams)) { - resultLogger.logRecommendationResult(request, response) - } - } - } else { - Stitch.value(RecommendationResponse(Nil)) - } - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/UserScoringService.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/UserScoringService.scala deleted file mode 100644 index b3a8c6664..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/UserScoringService.scala +++ /dev/null @@ -1,84 +0,0 @@ -package com.twitter.follow_recommendations.services - -import com.twitter.finagle.stats.Counter -import com.twitter.finagle.stats.StatsReceiver -import com.twitter.follow_recommendations.common.base.StatsUtil.profileStitchSeqResults -import com.twitter.follow_recommendations.common.clients.impression_store.WtfImpressionStore -import com.twitter.follow_recommendations.common.clients.socialgraph.SocialGraphClient -import com.twitter.follow_recommendations.common.rankers.ml_ranker.ranking.HydrateFeaturesTransform -import com.twitter.follow_recommendations.common.rankers.ml_ranker.ranking.MlRanker -import com.twitter.follow_recommendations.common.utils.RescueWithStatsUtils.rescueWithStats -import com.twitter.follow_recommendations.configapi.deciders.DeciderParams -import com.twitter.follow_recommendations.logging.FrsLogger -import com.twitter.follow_recommendations.models.ScoringUserRequest -import com.twitter.follow_recommendations.models.ScoringUserResponse -import com.twitter.stitch.Stitch -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class UserScoringService @Inject() ( - socialGraph: SocialGraphClient, - wtfImpressionStore: WtfImpressionStore, - hydrateFeaturesTransform: HydrateFeaturesTransform[ScoringUserRequest], - mlRanker: MlRanker[ScoringUserRequest], - resultLogger: FrsLogger, - stats: StatsReceiver) { - - private val scopedStats: StatsReceiver = stats.scope(this.getClass.getSimpleName) - private val disabledCounter: Counter = scopedStats.counter("disabled") - - def get(request: ScoringUserRequest): Stitch[ScoringUserResponse] = { - if (request.params(DeciderParams.EnableScoreUserCandidates)) { - val hydratedRequest = hydrate(request) - val candidatesStitch = hydratedRequest.flatMap { req => - hydrateFeaturesTransform.transform(req, request.candidates).flatMap { - candidateWithFeatures => - mlRanker.rank(req, candidateWithFeatures) - } - } - profileStitchSeqResults(candidatesStitch, scopedStats) - .map(ScoringUserResponse) - .onSuccess { response => - if (resultLogger.shouldLog(request.debugParams)) { - resultLogger.logScoringResult(request, response) - } - } - } else { - disabledCounter.incr() - Stitch.value(ScoringUserResponse(Nil)) - } - } - - private def hydrate(request: ScoringUserRequest): Stitch[ScoringUserRequest] = { - val allStitches = Stitch.collect(request.clientContext.userId.map { userId => - val recentFollowedUserIdsStitch = - rescueWithStats( - socialGraph.getRecentFollowedUserIds(userId), - stats, - "recentFollowedUserIds") - val recentFollowedByUserIdsStitch = - rescueWithStats( - socialGraph.getRecentFollowedByUserIds(userId), - stats, - "recentFollowedByUserIds") - val wtfImpressionsStitch = - rescueWithStats( - wtfImpressionStore.get(userId, request.displayLocation), - stats, - "wtfImpressions") - Stitch.join(recentFollowedUserIdsStitch, recentFollowedByUserIdsStitch, wtfImpressionsStitch) - }) - allStitches.map { - case Some((recentFollowedUserIds, recentFollowedByUserIds, wtfImpressions)) => - request.copy( - recentFollowedUserIds = - if (recentFollowedUserIds.isEmpty) None else Some(recentFollowedUserIds), - recentFollowedByUserIds = - if (recentFollowedByUserIds.isEmpty) None else Some(recentFollowedByUserIds), - wtfImpressions = if (wtfImpressions.isEmpty) None else Some(wtfImpressions) - ) - case _ => request - } - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/exceptions/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/exceptions/BUILD deleted file mode 100644 index 8b2c2d041..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/exceptions/BUILD +++ /dev/null @@ -1,14 +0,0 @@ -scala_library( - sources = ["*.scala"], - compiler_option_sets = ["fatal_warnings"], - tags = ["bazel-compatible"], - dependencies = [ - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift:controller", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift/exceptions", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift/filters", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift/modules", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift/response", - "finatra/thrift/src/main/scala/com/twitter/finatra/thrift/routing", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/exceptions/UnknownExceptionMapper.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/exceptions/UnknownExceptionMapper.scala deleted file mode 100644 index f3a09a6d7..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/services/exceptions/UnknownExceptionMapper.scala +++ /dev/null @@ -1,18 +0,0 @@ -package com.twitter.follow_recommendations.service.exceptions - -import com.twitter.finatra.thrift.exceptions.ExceptionMapper -import com.twitter.inject.Logging -import com.twitter.util.Future -import javax.inject.Singleton - -@Singleton -class UnknownLoggingExceptionMapper extends ExceptionMapper[Exception, Throwable] with Logging { - def handleException(throwable: Exception): Future[Throwable] = { - error( - s"Unmapped Exception: ${throwable.getMessage} - ${throwable.getStackTrace.mkString(", \n\t")}", - throwable - ) - - Future.exception(throwable) - } -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/BUILD b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/BUILD deleted file mode 100644 index e92976ad8..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/BUILD +++ /dev/null @@ -1,29 +0,0 @@ -scala_library( - compiler_option_sets = ["fatal_warnings"], - platform = "java8", - tags = ["bazel-compatible"], - dependencies = [ - "finatra/inject/inject-app/src/main/scala", - "finatra/inject/inject-core/src/main/scala", - "finatra/inject/inject-server/src/main/scala", - "finatra/inject/inject-thrift-client", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/base", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/addressbook", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/geo", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/ppmi_locale_follow", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/recent_engagement", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/sims_expansion", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/socialgraph", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/stp", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/triangular_loops", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/candidate_sources/two_hop_random_walk", - "follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/feature_hydration/sources", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/configapi/params", - "follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/models", - "twitter-server/server/src/main/scala", - "util/util-app/src/main/scala", - "util/util-core:scala", - "util/util-slf4j-api/src/main/scala", - ], -) diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/CandidateSourceHoldbackUtil.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/CandidateSourceHoldbackUtil.scala deleted file mode 100644 index 60a28f1a8..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/CandidateSourceHoldbackUtil.scala +++ /dev/null @@ -1,82 +0,0 @@ -package com.twitter.follow_recommendations.utils - -import com.twitter.follow_recommendations.common.candidate_sources.addressbook._ -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopCountrySource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopCountryBackFillSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeoSource -import com.twitter.follow_recommendations.common.candidate_sources.geo.PopGeohashSource -import com.twitter.follow_recommendations.common.candidate_sources.ppmi_locale_follow.PPMILocaleFollowSource -import com.twitter.follow_recommendations.common.candidate_sources.recent_engagement.RecentEngagementNonDirectFollowSource -import com.twitter.follow_recommendations.common.candidate_sources.sims.SwitchingSimsSource -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentEngagementSimilarUsersSource -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentFollowingSimilarUsersSource -import com.twitter.follow_recommendations.common.candidate_sources.sims_expansion.RecentStrongEngagementDirectFollowSimilarUsersSource -import com.twitter.follow_recommendations.common.candidate_sources.socialgraph.RecentFollowingRecentFollowingExpansionSource -import com.twitter.follow_recommendations.common.candidate_sources.stp.MutualFollowStrongTiePredictionSource -import com.twitter.follow_recommendations.common.candidate_sources.stp.OfflineStrongTiePredictionSource -import com.twitter.follow_recommendations.common.candidate_sources.stp.BaseOnlineSTPSource -import com.twitter.follow_recommendations.common.candidate_sources.stp.SocialProofEnforcedOfflineStrongTiePredictionSource -import com.twitter.follow_recommendations.common.candidate_sources.triangular_loops.TriangularLoopsSource -import com.twitter.follow_recommendations.common.candidate_sources.two_hop_random_walk.TwoHopRandomWalkSource -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.follow_recommendations.configapi.params.GlobalParams -import com.twitter.follow_recommendations.models.CandidateSourceType -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.timelines.configapi.HasParams - -trait CandidateSourceHoldbackUtil { - import CandidateSourceHoldbackUtil._ - def filterCandidateSources[T <: HasParams]( - request: T, - sources: Seq[CandidateSource[T, CandidateUser]] - ): Seq[CandidateSource[T, CandidateUser]] = { - val typeToFilter = request.params(GlobalParams.CandidateSourcesToFilter) - val sourcesToFilter = CandidateSourceTypeToMap.get(typeToFilter).getOrElse(Set.empty) - sources.filterNot { source => sourcesToFilter.contains(source.identifier) } - } -} - -object CandidateSourceHoldbackUtil { - final val ContextualActivityCandidateSourceIds: Set[CandidateSourceIdentifier] = - Set( - RecentFollowingSimilarUsersSource.Identifier, - RecentEngagementNonDirectFollowSource.Identifier, - RecentEngagementSimilarUsersSource.Identifier, - RecentStrongEngagementDirectFollowSimilarUsersSource.Identifier, - SwitchingSimsSource.Identifier, - ) - - final val SocialCandidateSourceIds: Set[CandidateSourceIdentifier] = - Set( - ForwardEmailBookSource.Identifier, - ForwardPhoneBookSource.Identifier, - ReverseEmailBookSource.Identifier, - ReversePhoneBookSource.Identifier, - RecentFollowingRecentFollowingExpansionSource.Identifier, - BaseOnlineSTPSource.Identifier, - MutualFollowStrongTiePredictionSource.Identifier, - OfflineStrongTiePredictionSource.Identifier, - SocialProofEnforcedOfflineStrongTiePredictionSource.Identifier, - TriangularLoopsSource.Identifier, - TwoHopRandomWalkSource.Identifier - ) - - final val GeoCandidateSourceIds: Set[CandidateSourceIdentifier] = - Set( - PPMILocaleFollowSource.Identifier, - PopCountrySource.Identifier, - PopGeohashSource.Identifier, - PopCountryBackFillSource.Identifier, - PopGeoSource.Identifier, - ) - - final val CandidateSourceTypeToMap: Map[CandidateSourceType.Value, Set[ - CandidateSourceIdentifier - ]] = - Map( - CandidateSourceType.Social -> SocialCandidateSourceIds, - CandidateSourceType.ActivityContextual -> ContextualActivityCandidateSourceIds, - CandidateSourceType.GeoAndInterests -> GeoCandidateSourceIds - ) -} diff --git a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/RecommendationFlowBaseSideEffectsUtil.scala b/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/RecommendationFlowBaseSideEffectsUtil.scala deleted file mode 100644 index 9304fb398..000000000 --- a/follow-recommendations-service/server/src/main/scala/com/twitter/follow_recommendations/utils/RecommendationFlowBaseSideEffectsUtil.scala +++ /dev/null @@ -1,121 +0,0 @@ -package com.twitter.follow_recommendations.utils - -import com.twitter.follow_recommendations.common.base.RecommendationFlow -import com.twitter.follow_recommendations.common.base.SideEffectsUtil -import com.twitter.follow_recommendations.common.models.CandidateUser -import com.twitter.product_mixer.core.functional_component.candidate_source.CandidateSource -import com.twitter.product_mixer.core.model.common.identifier.CandidateSourceIdentifier -import com.twitter.product_mixer.core.model.marshalling.request.HasClientContext -import com.twitter.snowflake.id.SnowflakeId -import com.twitter.stitch.Stitch - -trait RecommendationFlowBaseSideEffectsUtil[Target <: HasClientContext, Candidate <: CandidateUser] - extends SideEffectsUtil[Target, Candidate] { - recommendationFlow: RecommendationFlow[Target, Candidate] => - - override def applySideEffects( - target: Target, - candidateSources: Seq[CandidateSource[Target, Candidate]], - candidatesFromCandidateSources: Seq[Candidate], - mergedCandidates: Seq[Candidate], - filteredCandidates: Seq[Candidate], - rankedCandidates: Seq[Candidate], - transformedCandidates: Seq[Candidate], - truncatedCandidates: Seq[Candidate], - results: Seq[Candidate] - ): Stitch[Unit] = { - Stitch.async( - Stitch.collect( - Seq( - applySideEffectsCandidateSourceCandidates( - target, - candidateSources, - candidatesFromCandidateSources), - applySideEffectsMergedCandidates(target, mergedCandidates), - applySideEffectsFilteredCandidates(target, filteredCandidates), - applySideEffectsRankedCandidates(target, rankedCandidates), - applySideEffectsTransformedCandidates(target, transformedCandidates), - applySideEffectsTruncatedCandidates(target, truncatedCandidates), - applySideEffectsResults(target, results) - ) - )) - } - - /* - In subclasses, override functions below to apply custom side effects at each step in pipeline. - Call super.applySideEffectsXYZ to scribe basic scribes implemented in this parent class - */ - def applySideEffectsCandidateSourceCandidates( - target: Target, - candidateSources: Seq[CandidateSource[Target, Candidate]], - candidatesFromCandidateSources: Seq[Candidate] - ): Stitch[Unit] = { - val candidatesGroupedByCandidateSources = - candidatesFromCandidateSources.groupBy( - _.getPrimaryCandidateSource.getOrElse(CandidateSourceIdentifier("NoCandidateSource"))) - - target.getOptionalUserId match { - case Some(userId) => - val userAgeOpt = SnowflakeId.timeFromIdOpt(userId).map(_.untilNow.inDays) - userAgeOpt match { - case Some(userAge) if userAge <= 30 => - candidateSources.map { candidateSource => - { - val candidateSourceStats = statsReceiver.scope(candidateSource.identifier.name) - - val isEmpty = - !candidatesGroupedByCandidateSources.keySet.contains(candidateSource.identifier) - - if (userAge <= 1) - candidateSourceStats - .scope("user_age", "1", "empty").counter(isEmpty.toString).incr() - if (userAge <= 7) - candidateSourceStats - .scope("user_age", "7", "empty").counter(isEmpty.toString).incr() - if (userAge <= 30) - candidateSourceStats - .scope("user_age", "30", "empty").counter(isEmpty.toString).incr() - } - } - case _ => Nil - } - case None => Nil - } - Stitch.Unit - } - - def applySideEffectsBaseCandidates( - target: Target, - candidates: Seq[Candidate] - ): Stitch[Unit] = Stitch.Unit - - def applySideEffectsMergedCandidates( - target: Target, - candidates: Seq[Candidate] - ): Stitch[Unit] = applySideEffectsBaseCandidates(target, candidates) - - def applySideEffectsFilteredCandidates( - target: Target, - candidates: Seq[Candidate] - ): Stitch[Unit] = applySideEffectsBaseCandidates(target, candidates) - - def applySideEffectsRankedCandidates( - target: Target, - candidates: Seq[Candidate] - ): Stitch[Unit] = applySideEffectsBaseCandidates(target, candidates) - - def applySideEffectsTransformedCandidates( - target: Target, - candidates: Seq[Candidate] - ): Stitch[Unit] = applySideEffectsBaseCandidates(target, candidates) - - def applySideEffectsTruncatedCandidates( - target: Target, - candidates: Seq[Candidate] - ): Stitch[Unit] = applySideEffectsBaseCandidates(target, candidates) - - def applySideEffectsResults( - target: Target, - candidates: Seq[Candidate] - ): Stitch[Unit] = applySideEffectsBaseCandidates(target, candidates) -} diff --git a/follow-recommendations-service/thrift/src/main/thrift/BUILD b/follow-recommendations-service/thrift/src/main/thrift/BUILD deleted file mode 100644 index e5cbd19cf..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/BUILD +++ /dev/null @@ -1,21 +0,0 @@ -create_thrift_libraries( - base_name = "thrift", - sources = ["*.thrift"], - platform = "java8", - tags = ["bazel-compatible"], - dependency_roots = [ - "finatra-internal/thrift/src/main/thrift", - "follow-recommendations-service/thrift/src/main/thrift/logging:thrift", - "product-mixer/core/src/main/thrift/com/twitter/product_mixer/core:thrift", - "src/thrift/com/twitter/ads/adserver:adserver_common", - "src/thrift/com/twitter/ml/api:data", - "src/thrift/com/twitter/suggests/controller_data", - ], - generate_languages = [ - "java", - "scala", - "strato", - ], - provides_java_name = "follow-recommendations-java", - provides_scala_name = "follow-recommendations-scala", -) diff --git a/follow-recommendations-service/thrift/src/main/thrift/assembler.thrift b/follow-recommendations-service/thrift/src/main/thrift/assembler.thrift deleted file mode 100644 index eb782d0fe..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/assembler.thrift +++ /dev/null @@ -1,42 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -struct Header { - 1: required Title title -} - -struct Title { - 1: required string text -} - -struct Footer { - 1: optional Action action -} - -struct Action { - 1: required string text - 2: required string actionURL -} - -struct UserList { - 1: required bool userBioEnabled - 2: required bool userBioTruncated - 3: optional i64 userBioMaxLines - 4: optional FeedbackAction feedbackAction -} - -struct Carousel { - 1: optional FeedbackAction feedbackAction -} - -union WTFPresentation { - 1: UserList userBioList - 2: Carousel carousel -} - -struct DismissUserId {} - -union FeedbackAction { - 1: DismissUserId dismissUserId -} diff --git a/follow-recommendations-service/thrift/src/main/thrift/client_context.thrift b/follow-recommendations-service/thrift/src/main/thrift/client_context.thrift deleted file mode 100644 index adbdc407a..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/client_context.thrift +++ /dev/null @@ -1,19 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -// Caller/Client level specific context (e.g, user id/guest id/app id). -struct ClientContext { - 1: optional i64 userId(personalDataType='UserId') - 2: optional i64 guestId(personalDataType='GuestId') - 3: optional i64 appId(personalDataType='AppId') - 4: optional string ipAddress(personalDataType='IpAddress') - 5: optional string userAgent(personalDataType='UserAgent') - 6: optional string countryCode(personalDataType='InferredCountry') - 7: optional string languageCode(personalDataType='InferredLanguage') - 9: optional bool isTwoffice(personalDataType='InferredLocation') - 10: optional set userRoles - 11: optional string deviceId(personalDataType='DeviceId') - 12: optional i64 guestIdAds(personalDataType='GuestId') - 13: optional i64 guestIdMarketing(personalDataType='GuestId') -}(hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/debug.thrift b/follow-recommendations-service/thrift/src/main/thrift/debug.thrift deleted file mode 100644 index a41c59114..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/debug.thrift +++ /dev/null @@ -1,73 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendation - -// These are broken into their own union -// because we can have features that are -// complex flavors of these (such as Seq) -union PrimitiveFeatureValue { - 1: i32 intValue - 2: i64 longValue - 3: string strValue - 4: bool boolValue -} - -union FeatureValue { - 1: PrimitiveFeatureValue primitiveValue -} - -struct DebugParams { - 1: optional map featureOverrides - 2: optional i64 randomizationSeed - 3: optional bool includeDebugInfoInResults - 4: optional bool doNotLog -} - -enum DebugCandidateSourceIdentifier { - UTT_INTERESTS_RELATED_USERS_SOURCE = 0 - UTT_PRODUCER_EXPANSION_SOURCE = 1 - UTT_SEED_ACCOUNT_SOURCE = 2 - BYF_USER_FOLLOW_CLUSTER_SIMS_SOURCE = 3 - BYF_USER_FOLLOW_CLUSTER_SOURCE = 4 - USER_FOLLOW_CLUSTER_SOURCE = 5 - RECENT_SEARCH_BASED_SOURCE = 6 - PEOPLE_ACTIVITY_RECENT_ENGAGEMENT_SOURCE = 7 - PEOPLE_ACTIVITY_RECENT_ENGAGEMENT_SIMS_SOURCE = 8, - REVERSE_PHONE_BOOK_SOURCE = 9, - REVERSE_EMAIL_BOOK_SOURCE = 10, - SIMS_DEBUG_STORE = 11, - UTT_PRODUCER_ONLINE_MBCG_SOURCE = 12, - BONUS_FOLLOW_CONDITIONAL_ENGAGEMENT_STORE = 13, - // 14 (BONUS_FOLLOW_PMI_STORE) was deleted as it's not used anymore - FOLLOW2VEC_NEAREST_NEIGHBORS_STORE = 15, - OFFLINE_STP = 16, - OFFLINE_STP_BIG = 17, - OFFLINE_MUTUAL_FOLLOW_EXPANSION = 18, - REPEATED_PROFILE_VISITS = 19, - TIME_DECAY_FOLLOW2VEC_NEAREST_NEIGHBORS_STORE = 20, - LINEAR_REGRESSION_FOLLOW2VEC_NEAREST_NEIGHBORS_STORE = 21, - REAL_GRAPH_EXPANSION_SOURCE = 22, - RELATABLE_ACCOUNTS_BY_INTEREST = 23, - EMAIL_TWEET_CLICK = 24, - GOOD_TWEET_CLICK_ENGAGEMENTS = 25, - ENGAGED_FOLLOWER_RATIO = 26, - TWEET_SHARE_ENGAGEMENTS = 27, - BULK_FRIEND_FOLLOWS = 28, - REAL_GRAPH_OON_V2_SOURCE = 30, - CROWD_SEARCH_ACCOUNTS = 31, - POP_GEOHASH = 32, - POP_COUNTRY = 33, - POP_COUNTRY_BACKFILL = 34, - TWEET_SHARER_TO_SHARE_RECIPIENT_ENGAGEMENTS = 35, - TWEET_AUTHOR_TO_SHARE_RECIPIENT_ENGAGEMENTS = 36, - BULK_FRIEND_FOLLOWS_NEW_USER = 37, - ONLINE_STP_EPSCORER = 38, - ORGANIC_FOLLOW_ACCOUNTS = 39, - NUX_LO_HISTORY = 40, - TRAFFIC_ATTRIBUTION_ACCOUNTS = 41, - ONLINE_STP_RAW_ADDRESS_BOOK = 42, - POP_GEOHASH_QUALITY_FOLLOW = 43, - NOTIFICATION_ENGAGEMENT = 44, - EFR_BY_WORLDWIDE_PICTURE_PRODUCER = 45, - POP_GEOHASH_REAL_GRAPH = 46, -} diff --git a/follow-recommendations-service/thrift/src/main/thrift/display_context.thrift b/follow-recommendations-service/thrift/src/main/thrift/display_context.thrift deleted file mode 100644 index cfd613b71..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/display_context.thrift +++ /dev/null @@ -1,62 +0,0 @@ -include "flows.thrift" -include "recently_engaged_user_id.thrift" - -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -struct Profile { - 1: required i64 profileId(personalDataType='UserId') -}(hasPersonalData='true') - -struct Search { - 1: required string searchQuery(personalDataType='SearchQuery') -}(hasPersonalData='true') - -struct Rux { - 1: required i64 focalAuthorId(personalDataType='UserId') -}(hasPersonalData='true') - -struct Topic { - 1: required i64 topicId(personalDataType = 'TopicFollow') -}(hasPersonalData='true') - -struct ReactiveFollow { - 1: required list followedUserIds(personalDataType='UserId') -}(hasPersonalData='true') - -struct NuxInterests { - 1: optional flows.FlowContext flowContext // set for recommendation inside an interactive flow - 2: optional list uttInterestIds // if provided, we use these interestIds for generating candidates instead of for example fetching user selected interests -}(hasPersonalData='true') - -struct AdCampaignTarget { - 1: required list similarToUserIds(personalDataType='UserId') -}(hasPersonalData='true') - -struct ConnectTab { - 1: required list byfSeedUserIds(personalDataType='UserId') - 2: required list similarToUserIds(personalDataType='UserId') - 3: required list recentlyEngagedUserIds -}(hasPersonalData='true') - -struct SimilarToUser { - 1: required i64 similarToUserId(personalDataType='UserId') -}(hasPersonalData='true') - -struct PostNuxFollowTask { - 1: optional flows.FlowContext flowContext // set for recommendation inside an interactive flow -}(hasPersonalData='true') - -union DisplayContext { - 1: Profile profile - 2: Search search - 3: Rux rux - 4: Topic topic - 5: ReactiveFollow reactiveFollow - 6: NuxInterests nuxInterests - 7: AdCampaignTarget adCampaignTarget - 8: ConnectTab connectTab - 9: SimilarToUser similarToUser - 10: PostNuxFollowTask postNuxFollowTask -}(hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/display_location.thrift b/follow-recommendations-service/thrift/src/main/thrift/display_location.thrift deleted file mode 100644 index d94b9842e..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/display_location.thrift +++ /dev/null @@ -1,55 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -enum DisplayLocation { - SIDEBAR = 0 - PROFILE_SIDEBAR = 2 - CLUSTER_FOLLOW = 7 - NEW_USER_SARUS_BACKFILL = 12 - PROFILE_DEVICE_FOLLOW = 23 - RECOS_BACKFILL = 32 - HOME_TIMELINE = 39 # HOME_TIMELINE_WTF in Hermit - PROFILE_TOP_FOLLOWING = 42 - PROFILE_TOP_FOLLOWERS = 43 - PEOPLE_PLUS_PLUS = 47 - EXPLORE_TAB = 57 - MagicRecs = 59 # Account recommendation in notification - AB_UPLOAD_INJECTION = 60 - /** - * To prevent setting 2 display locations with the same index in FRS. - * - * The display location should be added to the following files: - * - follow-recommendations-service/thrift/src/main/thrift/display_location.thrift - * - follow-recommendations-service/thrift/src/main/thrift/logging/display_location.thrift - * - follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/DisplayLocation.scala - */ - CAMPAIGN_FORM = 61 - RUX_LANDING_PAGE = 62 - PROFILE_BONUS_FOLLOW = 63 - ELECTION_EXPLORE_WTF = 64 - HTL_BONUS_FOLLOW = 65 - TOPIC_LANDING_PAGE_HEADER = 66 - NUX_PYMK = 67 - NUX_INTERESTS = 68 - REACTIVE_FOLLOW = 69 - RUX_PYMK = 70 - INDIA_COVID19_CURATED_ACCOUNTS_WTF = 71 - NUX_TOPIC_BONUS_FOLLOW = 72 - TWEET_NOTIFICATION_RECS = 73 - HTL_SPACE_HOSTS = 74 - POST_NUX_FOLLOW_TASK = 75 - TOPIC_LANDING_PAGE = 76 - USER_TYPEAHEAD_PREFETCH = 77 - HOME_TIMELINE_RELATABLE_ACCOUNTS = 78 - NUX_GEO_CATEGORY = 79 - NUX_INTERESTS_CATEGORY = 80 - NUX_PYMK_CATEGORY = 81 - TOP_ARTICLES = 82 - HOME_TIMELINE_TWEET_RECS = 83 - HTL_BULK_FRIEND_FOLLOWS = 84 - NUX_AUTO_FOLLOW = 85 - SEARCH_BONUS_FOLLOW = 86 - CONTENT_RECOMMENDER = 87 - HOME_TIMELINE_REVERSE_CHRON = 88 -} diff --git a/follow-recommendations-service/thrift/src/main/thrift/engagementType.thrift b/follow-recommendations-service/thrift/src/main/thrift/engagementType.thrift deleted file mode 100644 index ef028d008..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/engagementType.thrift +++ /dev/null @@ -1,11 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -enum EngagementType { - Click = 0 - Like = 1 - Mention = 2 - Retweet = 3 - ProfileView = 4 -} diff --git a/follow-recommendations-service/thrift/src/main/thrift/flows.thrift b/follow-recommendations-service/thrift/src/main/thrift/flows.thrift deleted file mode 100644 index 894ebf81e..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/flows.thrift +++ /dev/null @@ -1,20 +0,0 @@ -/* - * This file defines additional thrift objects that should be specified in FRS request for context of recommendation, specifically the previous recommendations / new interactions in an interactive flow (series of follow steps). These typically are sent from OCF - */ - -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -struct FlowRecommendation { - 1: required i64 userId(personalDataType='UserId') -}(hasPersonalData='true') - -struct RecommendationStep { - 1: required list recommendations - 2: required set followedUserIds(personalDataType='UserId') -}(hasPersonalData='true') - -struct FlowContext { - 1: required list steps -}(hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/follow-recommendations-service.thrift b/follow-recommendations-service/thrift/src/main/thrift/follow-recommendations-service.thrift deleted file mode 100644 index 40aadc0b6..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/follow-recommendations-service.thrift +++ /dev/null @@ -1,100 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -include "assembler.thrift" -include "client_context.thrift" -include "debug.thrift" -include "display_context.thrift" -include "display_location.thrift" -include "recommendations.thrift" -include "recently_engaged_user_id.thrift" - -include "finatra-thrift/finatra_thrift_exceptions.thrift" -include "com/twitter/product_mixer/core/pipeline_execution_result.thrift" - -struct RecommendationRequest { - 1: required client_context.ClientContext clientContext - 2: required display_location.DisplayLocation displayLocation - 3: optional display_context.DisplayContext displayContext - // Max results to return - 4: optional i32 maxResults - // Cursor to continue returning results if any - 5: optional string cursor - // IDs of Content to exclude from recommendations - 6: optional list excludedIds(personalDataType='UserId') - // Whether to also get promoted content - 7: optional bool fetchPromotedContent - 8: optional debug.DebugParams debugParams - 9: optional string userLocationState(personalDataType='InferredLocation') -}(hasPersonalData='true') - - -struct RecommendationResponse { - 1: required list recommendations -}(hasPersonalData='true') - -// for scoring a list of candidates, while logging hydrated features -struct ScoringUserRequest { - 1: required client_context.ClientContext clientContext - 2: required display_location.DisplayLocation displayLocation - 3: required list candidates - 4: optional debug.DebugParams debugParams -}(hasPersonalData='true') - -struct ScoringUserResponse { - 1: required list candidates // empty for now -}(hasPersonalData='true') - -// for getting the list of candidates generated by a single candidate source -struct DebugCandidateSourceRequest { - 1: required client_context.ClientContext clientContext - 2: required debug.DebugCandidateSourceIdentifier candidateSource - 3: optional list uttInterestIds - 4: optional debug.DebugParams debugParams - 5: optional list recentlyFollowedUserIds - 6: optional list recentlyEngagedUserIds - 7: optional list byfSeedUserIds - 8: optional list similarToUserIds - 9: required bool applySgsPredicate - 10: optional i32 maxResults -}(hasPersonalData='true') - -service FollowRecommendationsThriftService { - RecommendationResponse getRecommendations(1: RecommendationRequest request) throws ( - 1: finatra_thrift_exceptions.ServerError serverError, - 2: finatra_thrift_exceptions.UnknownClientIdError unknownClientIdError, - 3: finatra_thrift_exceptions.NoClientIdError noClientIdError - ) - RecommendationDisplayResponse getRecommendationDisplayResponse(1: RecommendationRequest request) throws ( - 1: finatra_thrift_exceptions.ServerError serverError, - 2: finatra_thrift_exceptions.UnknownClientIdError unknownClientIdError, - 3: finatra_thrift_exceptions.NoClientIdError noClientIdError - ) - // temporary endpoint for feature hydration and logging for data collection. - ScoringUserResponse scoreUserCandidates(1: ScoringUserRequest request) throws ( - 1: finatra_thrift_exceptions.ServerError serverError, - 2: finatra_thrift_exceptions.UnknownClientIdError unknownClientIdError, - 3: finatra_thrift_exceptions.NoClientIdError noClientIdError - ) - // Debug endpoint for getting recommendations of a single candidate source. We can remove this endpoint when ProMix provide this functionality and we integrate with it. - RecommendationResponse debugCandidateSource(1: DebugCandidateSourceRequest request) throws ( - 1: finatra_thrift_exceptions.ServerError serverError, - 2: finatra_thrift_exceptions.UnknownClientIdError unknownClientIdError, - 3: finatra_thrift_exceptions.NoClientIdError noClientIdError - ) - - // Get the full execution log for a pipeline (used by our debugging tools) - pipeline_execution_result.PipelineExecutionResult executePipeline(1: RecommendationRequest request) throws ( - 1: finatra_thrift_exceptions.ServerError serverError, - 2: finatra_thrift_exceptions.UnknownClientIdError unknownClientIdError, - 3: finatra_thrift_exceptions.NoClientIdError noClientIdError - ) -} - -struct RecommendationDisplayResponse { - 1: required list hydratedRecommendation - 2: optional assembler.Header header - 3: optional assembler.Footer footer - 4: optional assembler.WTFPresentation wtfPresentation -}(hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/follow_recommendations_serving_history.thrift b/follow-recommendations-service/thrift/src/main/thrift/follow_recommendations_serving_history.thrift deleted file mode 100644 index 404b0ae29..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/follow_recommendations_serving_history.thrift +++ /dev/null @@ -1,9 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -// struct used for storing the history of computing and serving of recommendations to a user -struct FollowRecommendationsServingHistory { - 1: required i64 lastComputationTimeMs (personalDataType = 'PrivateTimestamp') - 2: required i64 lastServingTimeMs (personalDataType = 'PrivateTimestamp') -}(persisted='true', hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/BUILD b/follow-recommendations-service/thrift/src/main/thrift/logging/BUILD deleted file mode 100644 index 2f769d498..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/BUILD +++ /dev/null @@ -1,18 +0,0 @@ -create_thrift_libraries( - base_name = "thrift", - sources = ["*.thrift"], - platform = "java8", - tags = ["bazel-compatible"], - dependency_roots = [ - "src/thrift/com/twitter/ads/adserver:adserver_common", - "src/thrift/com/twitter/ml/api:data", - "src/thrift/com/twitter/suggests/controller_data", - ], - generate_languages = [ - "java", - "scala", - "strato", - ], - provides_java_name = "follow-recommendations-logging-java", - provides_scala_name = "follow-recommendations-logging-scala", -) diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/client_context.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/client_context.thrift deleted file mode 100644 index 2b6e454b2..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/client_context.thrift +++ /dev/null @@ -1,14 +0,0 @@ -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendations.logging - -// Offline equal of ClientContext -struct OfflineClientContext { - 1: optional i64 userId(personalDataType='UserId') - 2: optional i64 guestId(personalDataType='GuestId') - 3: optional i64 appId(personalDataType='AppId') - 4: optional string countryCode(personalDataType='InferredCountry') - 5: optional string languageCode(personalDataType='InferredLanguage') - 6: optional i64 guestIdAds(personalDataType='GuestId') - 7: optional i64 guestIdMarketing(personalDataType='GuestId') -}(persisted='true', hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/debug.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/debug.thrift deleted file mode 100644 index 882dca005..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/debug.thrift +++ /dev/null @@ -1,8 +0,0 @@ -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendation.logging - -// subset of DebugParams -struct OfflineDebugParams { - 1: optional i64 randomizationSeed // track if the request was randomly ranked or not -}(persisted='true', hasPersonalData='false') diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/display_context.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/display_context.thrift deleted file mode 100644 index c38850011..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/display_context.thrift +++ /dev/null @@ -1,66 +0,0 @@ -include "logging/flows.thrift" -include "logging/recently_engaged_user_id.thrift" - -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendations.logging - -// Offline equal of Profile DisplayContext -struct OfflineProfile { - 1: required i64 profileId(personalDataType='UserId') -}(persisted='true', hasPersonalData='true') - -// Offline equal of Search DisplayContext -struct OfflineSearch { - 1: required string searchQuery(personalDataType='SearchQuery') -}(persisted='true', hasPersonalData='true') - -// Offline equal of Rux Landing Page DisplayContext -struct OfflineRux { - 1: required i64 focalAuthorId(personalDataType="UserId") -}(persisted='true', hasPersonalData='true') - -// Offline equal of Topic DisplayContext -struct OfflineTopic { - 1: required i64 topicId(personalDataType = 'TopicFollow') -}(persisted='true', hasPersonalData='true') - -struct OfflineReactiveFollow { - 1: required list followedUserIds(personalDataType='UserId') -}(persisted='true', hasPersonalData='true') - -struct OfflineNuxInterests { - 1: optional flows.OfflineFlowContext flowContext // set for recommendation inside an interactive flow -}(persisted='true', hasPersonalData='true') - -struct OfflineAdCampaignTarget { - 1: required list similarToUserIds(personalDataType='UserId') -}(persisted='true', hasPersonalData='true') - -struct OfflineConnectTab { - 1: required list byfSeedUserIds(personalDataType='UserId') - 2: required list similarToUserIds(personalDataType='UserId') - 3: required list recentlyEngagedUserIds -}(persisted='true', hasPersonalData='true') - -struct OfflineSimilarToUser { - 1: required i64 similarToUserId(personalDataType='UserId') -}(persisted='true', hasPersonalData='true') - -struct OfflinePostNuxFollowTask { - 1: optional flows.OfflineFlowContext flowContext // set for recommendation inside an interactive flow -}(persisted='true', hasPersonalData='true') - -// Offline equal of DisplayContext -union OfflineDisplayContext { - 1: OfflineProfile profile - 2: OfflineSearch search - 3: OfflineRux rux - 4: OfflineTopic topic - 5: OfflineReactiveFollow reactiveFollow - 6: OfflineNuxInterests nuxInterests - 7: OfflineAdCampaignTarget adCampaignTarget - 8: OfflineConnectTab connectTab - 9: OfflineSimilarToUser similarToUser - 10: OfflinePostNuxFollowTask postNuxFollowTask -}(persisted='true', hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/display_location.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/display_location.thrift deleted file mode 100644 index a4dbbecd4..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/display_location.thrift +++ /dev/null @@ -1,55 +0,0 @@ -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendations.logging - -/** - * Make sure you add the new DL to the following files and redeploy our attribution jobs - * - follow-recommendations-service/thrift/src/main/thrift/display_location.thrift - * - follow-recommendations-service/thrift/src/main/thrift/logging/display_location.thrift - * - follow-recommendations-service/common/src/main/scala/com/twitter/follow_recommendations/common/models/DisplayLocation.scala - */ - -// Offline equal of DisplayLocation -enum OfflineDisplayLocation { - SIDEBAR = 0 - PROFILE_SIDEBAR = 2 - CLUSTER_FOLLOW = 7 - NEW_USER_SARUS_BACKFILL = 12 - PROFILE_DEVICE_FOLLOW = 23 - RECOS_BACKFILL = 32 - HOME_TIMELINE = 39 - PROFILE_TOP_FOLLOWING = 42 - PROFILE_TOP_FOLLOWERS = 43 - PEOPLE_PLUS_PLUS = 47 - EXPLORE_TAB = 57 - MagicRecs = 59 - AB_UPLOAD_INJECTION = 60 - CAMPAIGN_FORM = 61 - RUX_LANDING_PAGE = 62 - PROFILE_BONUS_FOLLOW = 63 - ELECTION_EXPLORE_WTF = 64 - HTL_BONUS_FOLLOW = 65 - TOPIC_LANDING_PAGE_HEADER = 66 - NUX_PYMK = 67 - NUX_INTERESTS = 68 - REACTIVE_FOLLOW = 69 - RUX_PYMK = 70 - INDIA_COVID19_CURATED_ACCOUNTS_WTF=71 - NUX_TOPIC_BONUS_FOLLOW = 72 - TWEET_NOTIFICATION_RECS = 73 - HTL_SPACE_HOSTS = 74 - POST_NUX_FOLLOW_TASK = 75 - TOPIC_LANDING_PAGE = 76 - USER_TYPEAHEAD_PREFETCH = 77 - HOME_TIMELINE_RELATABLE_ACCOUNTS = 78 - NUX_GEO_CATEGORY = 79 - NUX_INTERESTS_CATEGORY = 80 - NUX_PYMK_CATEGORY = 81 - TOP_ARTICLES = 82 - HOME_TIMELINE_TWEET_RECS = 83 - HTL_BULK_FRIEND_FOLLOWS = 84 - NUX_AUTO_FOLLOW = 85 - SEARCH_BONUS_FOLLOW = 86 - CONTENT_RECOMMENDER = 87 - HOME_TIMELINE_REVERSE_CHRON = 88 -}(persisted='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/engagementType.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/engagementType.thrift deleted file mode 100644 index 75191f16f..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/engagementType.thrift +++ /dev/null @@ -1,11 +0,0 @@ -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendations.logging - -enum EngagementType { - Click = 0 - Like = 1 - Mention = 2 - Retweet = 3 - ProfileView = 4 -} diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/flows.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/flows.thrift deleted file mode 100644 index 98551c08e..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/flows.thrift +++ /dev/null @@ -1,16 +0,0 @@ -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendations.logging - -struct OfflineFlowRecommendation { - 1: required i64 userId(personalDataType='UserId') -}(persisted='true', hasPersonalData='true') - -struct OfflineRecommendationStep { - 1: required list recommendations - 2: required set followedUserIds(personalDataType='UserId') -}(persisted='true', hasPersonalData='true') - -struct OfflineFlowContext { - 1: required list steps -}(persisted='true', hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/logs.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/logs.thrift deleted file mode 100644 index 33f09cfb9..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/logs.thrift +++ /dev/null @@ -1,72 +0,0 @@ -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendations.logging - -include "client_context.thrift" -include "debug.thrift" -include "display_context.thrift" -include "display_location.thrift" -include "recommendations.thrift" - -struct OfflineRecommendationRequest { - 1: required client_context.OfflineClientContext clientContext - 2: required display_location.OfflineDisplayLocation displayLocation - 3: optional display_context.OfflineDisplayContext displayContext - 4: optional i32 maxResults - 5: optional string cursor - 6: optional list excludedIds(personalDataType='UserId') - 7: optional bool fetchPromotedContent - 8: optional debug.OfflineDebugParams debugParams -}(persisted='true', hasPersonalData='true') - -struct OfflineRecommendationResponse { - 1: required list recommendations -}(persisted='true', hasPersonalData='true') - -struct RecommendationLog { - 1: required OfflineRecommendationRequest request - 2: required OfflineRecommendationResponse response - 3: required i64 timestampMs -}(persisted='true', hasPersonalData='true') - -struct OfflineScoringUserRequest { - 1: required client_context.OfflineClientContext clientContext - 2: required display_location.OfflineDisplayLocation displayLocation - 3: required list candidates -}(persisted='true', hasPersonalData='true') - -struct OfflineScoringUserResponse { - 1: required list candidates -}(persisted='true', hasPersonalData='true') - -struct ScoredUsersLog { - 1: required OfflineScoringUserRequest request - 2: required OfflineScoringUserResponse response - 3: required i64 timestampMs -}(persisted='true', hasPersonalData='true') - -struct OfflineRecommendationFlowUserMetadata { - 1: optional i32 userSignupAge(personalDataType = 'AgeOfAccount') - 2: optional string userState(personalDataType = 'UserState') -}(persisted='true', hasPersonalData='true') - -struct OfflineRecommendationFlowSignals { - 1: optional string countryCode(personalDataType='InferredCountry') -}(persisted='true', hasPersonalData='true') - -struct OfflineRecommendationFlowCandidateSourceCandidates { - 1: required string candidateSourceName - 2: required list candidateUserIds(personalDataType='UserId') - 3: optional list candidateUserScores -}(persisted='true', hasPersonalData='true') - -struct RecommendationFlowLog { - 1: required client_context.OfflineClientContext clientContext - 2: optional OfflineRecommendationFlowUserMetadata userMetadata - 3: optional OfflineRecommendationFlowSignals signals - 4: required i64 timestampMs - 5: required string recommendationFlowIdentifier - 6: optional list filteredCandidates - 7: optional list rankedCandidates - 8: optional list truncatedCandidates -}(persisted='true', hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/reasons.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/reasons.thrift deleted file mode 100644 index 6fc24d919..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/reasons.thrift +++ /dev/null @@ -1,62 +0,0 @@ -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendations.logging - -// Proof based on Follow relationship -struct FollowProof { - 1: required list userIds(personalDataType='UserId') - 2: required i32 numIds(personalDataType='CountOfFollowersAndFollowees') -}(persisted='true', hasPersonalData='true') - -// Similar to userIds in the context (e.g. profileId) -struct SimilarToProof { - 1: required list userIds(personalDataType='UserId') -}(persisted='true', hasPersonalData='true') - -// Proof based on geo location -struct PopularInGeoProof { - 1: required string location(personalDataType='InferredLocation') -}(persisted='true', hasPersonalData='true') - -// Proof based on ttt interest -struct TttInterestProof { - 1: required i64 interestId(personalDataType='ProvidedInterests') - 2: required string interestDisplayName(personalDataType='ProvidedInterests') -}(persisted='true', hasPersonalData='true') - -// Proof based on topics -struct TopicProof { - 1: required i64 topicId(personalDataType='ProvidedInterests') -}(persisted='true', hasPersonalData='true') - -// Proof based on custom interest / search queries -struct CustomInterestProof { - 1: required string customerInterest(personalDataType='SearchQuery') -}(persisted='true', hasPersonalData='true') - -// Proof based on tweet authors -struct TweetsAuthorProof { - 1: required list tweetIds(personalDataType='TweetId') -}(persisted='true', hasPersonalData='true') - -// Proof candidate is of device follow type -struct DeviceFollowProof { - 1: required bool isDeviceFollow(personalDataType='OtherDeviceInfo') -}(persisted='true', hasPersonalData='true') - -// Account level proof that should be attached to each candidate -struct AccountProof { - 1: optional FollowProof followProof - 2: optional SimilarToProof similarToProof - 3: optional PopularInGeoProof popularInGeoProof - 4: optional TttInterestProof tttInterestProof - 5: optional TopicProof topicProof - 6: optional CustomInterestProof customInterestProof - 7: optional TweetsAuthorProof tweetsAuthorProof - 8: optional DeviceFollowProof deviceFollowProof - -}(persisted='true', hasPersonalData='true') - -struct Reason { - 1: optional AccountProof accountProof -}(persisted='true', hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/recently_engaged_user_id.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/recently_engaged_user_id.thrift deleted file mode 100644 index f0af960b9..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/recently_engaged_user_id.thrift +++ /dev/null @@ -1,10 +0,0 @@ -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendations.logging - -include "engagementType.thrift" - -struct RecentlyEngagedUserId { - 1: required i64 id(personalDataType='UserId') - 2: required engagementType.EngagementType engagementType -}(persisted='true', hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/recommendations.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/recommendations.thrift deleted file mode 100644 index bf94e41b8..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/recommendations.thrift +++ /dev/null @@ -1,26 +0,0 @@ -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendations.logging - -include "com/twitter/ads/adserver/adserver_common.thrift" -include "reasons.thrift" -include "tracking.thrift" -include "scoring.thrift" - -// Offline equal of UserRecommendation -struct OfflineUserRecommendation { - 1: required i64 userId(personalDataType='UserId') - // reason for this suggestions, eg: social context - 2: optional reasons.Reason reason - // present if it is a promoted account - 3: optional adserver_common.AdImpression adImpression - // tracking token (unserialized) for attribution - 4: optional tracking.TrackingToken trackingToken - // scoring details - 5: optional scoring.ScoringDetails scoringDetails -}(persisted='true', hasPersonalData='true') - -// Offline equal of Recommendation -union OfflineRecommendation { - 1: OfflineUserRecommendation user -}(persisted='true', hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/scoring.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/scoring.thrift deleted file mode 100644 index e1524662d..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/scoring.thrift +++ /dev/null @@ -1,38 +0,0 @@ -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendations.logging - -include "com/twitter/ml/api/data.thrift" - -struct CandidateSourceDetails { - 1: optional map candidateSourceScores - 2: optional i32 primarySource -}(persisted='true', hasPersonalData='false') - -struct Score { - 1: required double value - 2: optional string rankerId - 3: optional string scoreType -}(persisted='true', hasPersonalData='false') // scoring and ranking info per ranking stage - -// Contains (1) the ML-based heavy ranker and score (2) scores and rankers in producer experiment framework -struct Scores { - 1: required list scores - 2: optional string selectedRankerId - 3: required bool isInProducerScoringExperiment -}(persisted='true', hasPersonalData='false') - -struct RankingInfo { - 1: optional Scores scores - 2: optional i32 rank -}(persisted='true', hasPersonalData='false') - -// this encapsulates all information related to the ranking process from generation to scoring -struct ScoringDetails { - 1: optional CandidateSourceDetails candidateSourceDetails - 2: optional double score // The ML-based heavy ranker score - 3: optional data.DataRecord dataRecord - 4: optional list rankerIds // all ranker ids, including (1) ML-based heavy ranker (2) non-ML adhoc rankers - 5: optional map infoPerRankingStage // scoring and ranking info per ranking stage -}(persisted='true', hasPersonalData='true') - diff --git a/follow-recommendations-service/thrift/src/main/thrift/logging/tracking.thrift b/follow-recommendations-service/thrift/src/main/thrift/logging/tracking.thrift deleted file mode 100644 index 067ba1a46..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/logging/tracking.thrift +++ /dev/null @@ -1,16 +0,0 @@ -namespace java com.twitter.follow_recommendations.logging.thriftjava -#@namespace scala com.twitter.follow_recommendations.logging.thriftscala -#@namespace strato com.twitter.follow_recommendations.logging - -include "com/twitter/suggests/controller_data/controller_data.thrift" -include "display_location.thrift" - -struct TrackingToken { - // trace-id of the request - 1: required i64 sessionId (personalDataType='SessionId') - 2: optional display_location.OfflineDisplayLocation displayLocation - // 64-bit encoded binary attributes of our recommendation - 3: optional controller_data.ControllerData controllerData - // WTF Algorithm Id (backward compatibility) - 4: optional i32 algoId -}(persisted='true', hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/reasons.thrift b/follow-recommendations-service/thrift/src/main/thrift/reasons.thrift deleted file mode 100644 index 299e88885..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/reasons.thrift +++ /dev/null @@ -1,61 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -// Proof based on Follow relationship -struct FollowProof { - 1: required list userIds(personalDataType='UserId') - 2: required i32 numIds(personalDataType='CountOfFollowersAndFollowees') -}(hasPersonalData='true') - -// Similar to userIds in the context (e.g. profileId) -struct SimilarToProof { - 1: required list userIds(personalDataType='UserId') -}(hasPersonalData='true') - -// Proof based on geo location -struct PopularInGeoProof { - 1: required string location(personalDataType='InferredLocation') -}(hasPersonalData='true') - -// Proof based on ttt interest -struct TttInterestProof { - 1: required i64 interestId(personalDataType='ProvidedInterests') - 2: required string interestDisplayName(personalDataType='ProvidedInterests') -}(hasPersonalData='true') - -// Proof based on topics -struct TopicProof { - 1: required i64 topicId(personalDataType='ProvidedInterests') -}(hasPersonalData='true') - -// Proof based on custom interest / search queries -struct CustomInterestProof { - 1: required string query(personalDataType='SearchQuery') -}(hasPersonalData='true') - -// Proof based on tweet authors -struct TweetsAuthorProof { - 1: required list tweetIds(personalDataType='TweetId') -}(hasPersonalData='true') - -// Proof candidate is of device follow type -struct DeviceFollowProof { - 1: required bool isDeviceFollow(personalDataType='OtherDeviceInfo') -}(hasPersonalData='true') - -// Account level proof that should be attached to each candidate -struct AccountProof { - 1: optional FollowProof followProof - 2: optional SimilarToProof similarToProof - 3: optional PopularInGeoProof popularInGeoProof - 4: optional TttInterestProof tttInterestProof - 5: optional TopicProof topicProof - 6: optional CustomInterestProof customInterestProof - 7: optional TweetsAuthorProof tweetsAuthorProof - 8: optional DeviceFollowProof deviceFollowProof -}(hasPersonalData='true') - -struct Reason { - 1: optional AccountProof accountProof -}(hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/recently_engaged_user_id.thrift b/follow-recommendations-service/thrift/src/main/thrift/recently_engaged_user_id.thrift deleted file mode 100644 index 6a13bd31e..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/recently_engaged_user_id.thrift +++ /dev/null @@ -1,10 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -include "engagementType.thrift" - -struct RecentlyEngagedUserId { - 1: required i64 id(personalDataType='UserId') - 2: required engagementType.EngagementType engagementType -}(persisted='true', hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/recommendations.thrift b/follow-recommendations-service/thrift/src/main/thrift/recommendations.thrift deleted file mode 100644 index 1070bb11c..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/recommendations.thrift +++ /dev/null @@ -1,40 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -include "com/twitter/ads/adserver/adserver_common.thrift" -include "debug.thrift" -include "reasons.thrift" -include "scoring.thrift" - -struct UserRecommendation { - 1: required i64 userId(personalDataType='UserId') - // reason for this suggestions, eg: social context - 2: optional reasons.Reason reason - // present if it is a promoted account - 3: optional adserver_common.AdImpression adImpression - // tracking token for attribution - 4: optional string trackingInfo - // scoring details - 5: optional scoring.ScoringDetails scoringDetails - 6: optional string recommendationFlowIdentifier - // FeatureSwitch overrides for candidates: - 7: optional map featureOverrides -}(hasPersonalData='true') - -union Recommendation { - 1: UserRecommendation user -}(hasPersonalData='true') - -struct HydratedUserRecommendation { - 1: required i64 userId(personalDataType='UserId') - 2: optional string socialProof - // present if it is a promoted account, used by clients for determining ad impression - 3: optional adserver_common.AdImpression adImpression - // tracking token for attribution - 4: optional string trackingInfo -}(hasPersonalData='true') - -union HydratedRecommendation { - 1: HydratedUserRecommendation hydratedUserRecommendation -} diff --git a/follow-recommendations-service/thrift/src/main/thrift/scoring.thrift b/follow-recommendations-service/thrift/src/main/thrift/scoring.thrift deleted file mode 100644 index 33111baf8..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/scoring.thrift +++ /dev/null @@ -1,49 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -include "com/twitter/ml/api/data.thrift" - -struct CandidateSourceDetails { - 1: optional map candidateSourceScores - 2: optional i32 primarySource - 3: optional map candidateSourceRanks -}(hasPersonalData='false') - -struct Score { - 1: required double value - 2: optional string rankerId - 3: optional string scoreType -}(hasPersonalData='false') - -// Contains (1) the ML-based heavy ranker and score (2) scores and rankers in producer experiment framework -struct Scores { - 1: required list scores - 2: optional string selectedRankerId - 3: required bool isInProducerScoringExperiment -}(hasPersonalData='false') - -struct RankingInfo { - 1: optional Scores scores - 2: optional i32 rank -}(hasPersonalData='false') - -// this encapsulates all information related to the ranking process from generation to scoring -struct ScoringDetails { - 1: optional CandidateSourceDetails candidateSourceDetails - 2: optional double score - 3: optional data.DataRecord dataRecord - 4: optional list rankerIds - 5: optional DebugDataRecord debugDataRecord // this field is not logged as it's only used for debugging - 6: optional map infoPerRankingStage // scoring and ranking info per ranking stage -}(hasPersonalData='true') - -// exactly the same as a data record, except that we store the feature name instead of the id -struct DebugDataRecord { - 1: optional set binaryFeatures; // stores BINARY features - 2: optional map continuousFeatures; // stores CONTINUOUS features - 3: optional map discreteFeatures; // stores DISCRETE features - 4: optional map stringFeatures; // stores STRING features - 5: optional map> sparseBinaryFeatures; // stores sparse BINARY features - 6: optional map> sparseContinuousFeatures; // sparse CONTINUOUS features -}(hasPersonalData='true') diff --git a/follow-recommendations-service/thrift/src/main/thrift/tracking.thrift b/follow-recommendations-service/thrift/src/main/thrift/tracking.thrift deleted file mode 100644 index 81111ead8..000000000 --- a/follow-recommendations-service/thrift/src/main/thrift/tracking.thrift +++ /dev/null @@ -1,17 +0,0 @@ -namespace java com.twitter.follow_recommendations.thriftjava -#@namespace scala com.twitter.follow_recommendations.thriftscala -#@namespace strato com.twitter.follow_recommendations - -include "com/twitter/suggests/controller_data/controller_data.thrift" -include "display_location.thrift" - -// struct used for tracking/attribution purposes in our offline pipelines -struct TrackingToken { - // trace-id of the request - 1: required i64 sessionId (personalDataType='SessionId') - 2: optional display_location.DisplayLocation displayLocation - // 64-bit encoded binary attributes of our recommendation - 3: optional controller_data.ControllerData controllerData - // WTF Algorithm Id (backward compatibility) - 4: optional i32 algoId -}(hasPersonalData='true')