/* * 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.similarity.item; import io.seldon.clustering.recommender.ItemRecommendationAlgorithm; import io.seldon.clustering.recommender.ItemRecommendationResultSet; import io.seldon.clustering.recommender.RecommendationContext; import io.seldon.memcache.DogpileHandler; import io.seldon.memcache.MemCacheKeys; import io.seldon.memcache.MemCachePeer; import io.seldon.memcache.UpdateRetriever; import io.seldon.recommendation.RecommendationUtils; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.log4j.Logger; import org.springframework.stereotype.Component; @Component public class ItemSimilarityRecommender implements ItemRecommendationAlgorithm { private static final String name = ItemSimilarityRecommender.class.getSimpleName(); private static final String RECENT_ACTIONS_PROPERTY_NAME = "io.seldon.algorithm.general.numrecentactionstouse"; private static Logger logger = Logger.getLogger( ItemSimilarityRecommender.class.getName() ); final static int RECOMMEND_CACHE_TIME_SECS = 3600; public Map<Long,Double> recommendSimilarItems(final String client, final long itemId, final Set<Integer> dimensions, int numRecommendations, Set<Long> exclusions, boolean rescaleScores) { String memKey = MemCacheKeys.getItemSimilarity(client, itemId, dimensions, -1); Map<Long,Double> res = (Map<Long,Double>) MemCachePeer.get(memKey); Map<Long, Double> newRes = null; try{ newRes = DogpileHandler.get().retrieveUpdateIfRequired(memKey, res, new UpdateRetriever<Map<Long, Double>>() { @Override public Map<Long, Double> retrieve() throws Exception { IItemSimilarityPeer peer = new JdoItemSimilarityPeer(client); return peer.getSimilarItems(itemId, dimensions, -1); } },RECOMMEND_CACHE_TIME_SECS); } catch (Exception e){ logger.warn("Error when retrieving similar items in dogpile handler ", e); } if (newRes != null) { MemCachePeer.put(memKey, newRes, RECOMMEND_CACHE_TIME_SECS); res = newRes; } if(newRes==null && res == null) { if (logger.isDebugEnabled()) logger.info("No similar item recommendation results for item "+itemId+" dimension "+StringUtils.join(dimensions, ",")+" for client "+client); return new HashMap<>(); } for(Long excl : exclusions) res.remove(excl); if (rescaleScores) return RecommendationUtils.rescaleScoresToOne(res, numRecommendations); else return res; } public List<ItemRecommendationResultSet.ItemRecommendationResult> recommendSimilarItems( String client, List<Long> items, Set<Integer> dimensions, int numRecommendations, Set<Long> exclusions) { List<ItemRecommendationResultSet.ItemRecommendationResult> toReturn = new ArrayList<>(); Map<Long,Double> res = new HashMap<>(); for(Long itemId : items) { if (logger.isDebugEnabled()) logger.debug("Finding similar items to "+itemId); Map<Long,Double> scores = recommendSimilarItems(client,itemId, dimensions, numRecommendations, exclusions,false); for(Map.Entry<Long,Double> score : scores.entrySet()) { if (res.containsKey(score.getKey())) res.put(score.getKey(), score.getValue()+res.get(score.getKey())); else res.put(score.getKey(), score.getValue()); } } for (Map.Entry<Long, Double> entry : res.entrySet()) { toReturn.add(new ItemRecommendationResultSet.ItemRecommendationResult(entry.getKey(),entry.getValue().floatValue())); } logger.info("Found "+res.size()+" similar items based on history of "+items.size()); return toReturn; } @Override public ItemRecommendationResultSet recommend(String client, Long user, Set<Integer> dimensions, int maxRecsCount, RecommendationContext ctxt, List<Long> recentItemInteractions) { RecommendationContext.OptionsHolder opts = ctxt.getOptsHolder(); int numRecentActionsToUse = opts.getIntegerOption(RECENT_ACTIONS_PROPERTY_NAME); List<Long> itemsToScore; if(recentItemInteractions.size() > numRecentActionsToUse) { if (logger.isDebugEnabled()) logger.debug("Limiting recent items for score to size "+numRecentActionsToUse+" from present "+recentItemInteractions.size()); itemsToScore = recentItemInteractions.subList(0, numRecentActionsToUse); } else itemsToScore = new ArrayList<>(recentItemInteractions); Set<Long> exclusions = ctxt.getContextItems(); List<ItemRecommendationResultSet.ItemRecommendationResult> recommendations = null; if (itemsToScore.size() > 0) { recommendations = recommendSimilarItems(client, itemsToScore, dimensions, maxRecsCount, exclusions); if (recommendations != null) logger.info("Recent similar items recommender returned " + recommendations.size() + " recommendations"); } else logger.info("failing RECENT_SIMILAR_ITEMS as no recent actions"); return new ItemRecommendationResultSet(recommendations, name); } @Override public String name() { return name; } }