/* * Seldon -- open source prediction engine * ======================================= * * Copyright 2011-2015 Seldon Technologies Ltd and Rummble Ltd (http://www.seldon.io/) * * ******************************************************************************************** * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * ******************************************************************************************** */ package io.seldon.recommendation; import io.seldon.api.APIException; import io.seldon.api.Constants; import io.seldon.api.caching.ActionHistoryProvider; import io.seldon.api.state.ClientAlgorithmStore; import io.seldon.api.state.options.DefaultOptions; import io.seldon.clustering.recommender.ItemRecommendationResultSet; import io.seldon.clustering.recommender.RecommendationContext; import io.seldon.clustering.recommender.jdo.JdoCountRecommenderUtils; import io.seldon.recommendation.combiner.AlgorithmResultsCombiner; import io.seldon.recommendation.filters.ExplicitItemsIncluder; import io.seldon.recommendation.filters.FilteredItems; import io.seldon.util.CollectionTools; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; /** * Core recommendation algorithm selection. Calls particular classes and methods to carry out the various API methods * @author rummble * */ @Component public class RecommendationPeer { ; private static Logger logger = Logger.getLogger(RecommendationPeer.class.getName()); public static final int MEMCACHE_TRUSTNET_EXPIRE_SECS = 60 * 60 * 3; public static final int MEMCACHE_RECENT_ITEMS_EXPIRE_SECS = 60 * 15; private final DefaultOptions defaultOptions; private double HIGH_RATING_THRESHOLD = 0.5; private double INITIAL_REVIEW_SET_RATIO = 2F; private ClientAlgorithmStore algStore; @Autowired JdoCountRecommenderUtils cUtils; @Autowired ActionHistoryProvider actionProvider; @Autowired ExplicitItemsIncluder explicitItemsIncluder; private boolean debugging = false; @Autowired public RecommendationPeer(ClientAlgorithmStore algStore, DefaultOptions defaultOptions) { this.algStore = algStore; this.defaultOptions = defaultOptions; } public ImmutablePair<RecommendationResult, RecResultContext> getRecommendations(long user, String client, String clientUserId, Integer type, Set<Integer> dimensions, int numRecommendationsAsked, String lastRecListUUID, Long currentItemId, String referrer, String recTag, List<String> algorithmOverride,Set<Long> scoreItems) { ClientStrategy strategy; if (algorithmOverride != null && !algorithmOverride.isEmpty()) { logger.debug("Overriding algorithms from JS"); strategy = algStore.retrieveStrategy(client, algorithmOverride); } else { strategy = algStore.retrieveStrategy(client); } if (strategy == null) { throw new APIException(APIException.NOT_VALID_STRATEGY); } //Set base values - will be used for anonymous users int numRecommendations = numRecommendationsAsked; int numRecentActions = 0; Double diversityLevel = strategy.getDiversityLevel(clientUserId, recTag); if (diversityLevel > 1.0f) { int numRecommendationsDiverse = new Long(Math.round(numRecommendationsAsked * diversityLevel)).intValue(); if (debugging) logger.debug("Updated num recommendations as for client "+client+" diversity is "+diversityLevel+" was "+numRecommendationsAsked+" will now be "+numRecommendationsDiverse); numRecommendations = numRecommendationsDiverse; } else numRecommendations = numRecommendationsAsked; List<Long> recentActions = null; if (user != Constants.ANONYMOUS_USER) // only can get recent actions for non anonymous user { //TODO - fix limit recentActions = actionProvider.getRecentActions(client,user, 100); numRecentActions = recentActions.size(); if (debugging) logger.debug("RecentActions for user with client "+client+" internal user id "+user+" num." + numRecentActions); } else if (debugging) logger.debug("Can't get recent actions for anonymous user "+clientUserId); Map<Long,Double> recommenderScores = new HashMap<>(); List<String> algsUsed = new ArrayList<>(); List<RecResultContext> resultSets = new ArrayList<>(); AlgorithmResultsCombiner combiner = strategy.getAlgorithmResultsCombiner(clientUserId, recTag); for(AlgorithmStrategy algStr : strategy.getAlgorithms(clientUserId, recTag)) { long startTime = System.currentTimeMillis(); if (logger.isDebugEnabled()) logger.debug("Using recommender class " + algStr.name); List<Long> recentItemInteractions; // add items from recent history if there are any and algorithm options says to use them if (recentActions != null && recentActions.size() > 0) recentItemInteractions = new ArrayList<>(recentActions); else recentItemInteractions = new ArrayList<>(); // add current item id if not in recent actions if (currentItemId != null && !recentItemInteractions.contains(currentItemId)) recentItemInteractions.add(0,currentItemId); FilteredItems explicitItems = null; if (scoreItems != null) explicitItems = explicitItemsIncluder.create(client, scoreItems); RecommendationContext ctxt = RecommendationContext.buildContext(client, algStr,user,clientUserId,currentItemId, dimensions, lastRecListUUID, numRecommendations,defaultOptions,explicitItems); ItemRecommendationResultSet results = algStr.algorithm.recommend(client, user, dimensions, numRecommendations, ctxt, recentItemInteractions); if (logger.isDebugEnabled()) logger.debug("Recommender "+algStr.name+" returned "+results.getResults().size()+" results, took "+(System.currentTimeMillis()-startTime) + "ms"); resultSets.add(new RecResultContext(results, results.getRecommenderName())); if(combiner.isEnoughResults(numRecommendationsAsked, resultSets)) break; } RecResultContext combinedResults = combiner.combine(numRecommendations, resultSets); if (logger.isDebugEnabled()) logger.debug("After combining, we have "+combinedResults.resultSet.getResults().size()+ " results with alg key "+combinedResults.algKey + " : " + StringUtils.join(combinedResults.resultSet.getResults(),':')); for (ItemRecommendationResultSet.ItemRecommendationResult result : combinedResults.resultSet.getResults()) { recommenderScores.put(result.item, result.score.doubleValue()); } if (recommenderScores.size() > 0) { // switch(options.getPostprocessing()) // { // case REORDER_BY_POPULARITY: // { // IBaselineRecommenderUtils baselineUtils = new SqlBaselineRecommenderUtils(options.getName()); // BaselineRecommender br = new BaselineRecommender(options.getName(), baselineUtils); // recommenderScores = br.reorderRecommendationsByPopularity(recommenderScores); // } // break; // default: // break; // } List<Long> recommendationsFinal = CollectionTools.sortMapAndLimitToList(recommenderScores, numRecommendations, true); if (logger.isDebugEnabled()) logger.debug("recommendationsFinal size was " +recommendationsFinal.size()); final RecommendationResult recommendationResult = createFinalRecResult(numRecommendationsAsked, client, clientUserId, dimensions, lastRecListUUID, recommendationsFinal, combinedResults.algKey, currentItemId, numRecentActions, diversityLevel,strategy,recTag); final ImmutablePair<RecommendationResult, RecResultContext> retVal = new ImmutablePair<>(recommendationResult, combinedResults); return retVal; } else { logger.warn("Returning no recommendations for user with client id "+clientUserId); final RecommendationResult recommendationResult = createFinalRecResult(numRecommendationsAsked,client, clientUserId, dimensions, lastRecListUUID, new ArrayList<Long>(),"",currentItemId, numRecentActions, diversityLevel,strategy, recTag); final String algKey = ""; final ImmutablePair<RecommendationResult, RecResultContext> retVal = new ImmutablePair<>(recommendationResult, combinedResults); return retVal; } } private RecommendationResult createFinalRecResult(int numRecommendationsAsked, String client, String clientUserId, Set<Integer> dimensions,String currentRecUUID,List<Long> recs,String algKey, Long currentItemId,int numRecentActions, Double diversityLevel, ClientStrategy strat, String recTag) { List<Long> recsFinal; if (diversityLevel > 1.0) recsFinal = RecommendationUtils.getDiverseRecommendations(numRecommendationsAsked, recs,client,clientUserId,dimensions); else recsFinal = recs; if (logger.isDebugEnabled()) logger.debug("recs final size "+ recsFinal.size()); String uuid=RecommendationUtils.cacheRecommendationsAndCreateNewUUID(client, clientUserId, dimensions, currentRecUUID, recsFinal, algKey,currentItemId,numRecentActions, strat, recTag); List<Recommendation> recBeans = new ArrayList<>(); for(Long itemId : recsFinal) recBeans.add(new Recommendation(itemId, 0, 0.0)); return new RecommendationResult(recBeans, uuid, strat.getName(clientUserId,recTag)); } public static class RecResultContext { public static final RecResultContext EMPTY = new RecResultContext(new ItemRecommendationResultSet("UNKNOWN"), "UNKNOWN"); public final ItemRecommendationResultSet resultSet; public final String algKey; public Map<Long,String> item_recommender_lookup = null; public RecResultContext(ItemRecommendationResultSet resultSet, String algKey) { this.resultSet = resultSet; this.algKey = algKey; } } }