/* * Copyright 2011 Research Studios Austria Forschungsgesellschaft mBH * * This file is part of easyrec. * * easyrec is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * easyrec is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with easyrec. If not, see <http://www.gnu.org/licenses/>. */ package org.easyrec.plugin.slopeone.impl; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.easyrec.model.core.ItemAssocVO; import org.easyrec.model.core.RatingVO; import org.easyrec.model.core.TenantVO; import org.easyrec.plugin.slopeone.DeviationCalculationStrategy; import org.easyrec.plugin.slopeone.SlopeOneService; import org.easyrec.plugin.slopeone.model.*; import org.easyrec.plugin.slopeone.store.dao.ActionDAO; import org.easyrec.plugin.slopeone.store.dao.DeviationDAO; import org.easyrec.plugin.support.ExecutablePluginSupport; import org.easyrec.plugin.support.ExecutablePluginSupport.ExecutionControl; import org.easyrec.store.dao.core.ItemAssocDAO; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; /** * Implementation of SlopeOneService.<p><b>Company: </b> SAT, Research Studios Austria</p> * <p><b>Copyright: </b> (c) 2007</p> <p><b>last modified:</b><br/> $Author: dmann $<br/> $Date: 2011-12-20 15:22:22 +0100 (Di, 20 Dez 2011) $<br/> $Revision: 18685 $</p> * * @author Patrick Marschik */ public class SlopeOneServiceImpl implements SlopeOneService { private static final Log logger = LogFactory.getLog(SlopeOneServiceImpl.class); private ActionDAO actionDAO; private DeviationCalculationStrategy deviationCalculation; private DeviationDAO deviationDAO; private ItemAssocDAO itemAssocDAO; public SlopeOneServiceImpl(ItemAssocDAO itemAssocDAO, ActionDAO actionDAO, DeviationDAO deviationDAO, DeviationCalculationStrategy deviationCalculation) { this.itemAssocDAO = itemAssocDAO; this.actionDAO = actionDAO; this.deviationDAO = deviationDAO; this.deviationCalculation = deviationCalculation; } public void calculateDeviations(SlopeOneIntegerConfiguration config, Date lastRun, SlopeOneStats stats, Set<TenantItem> changedItemIds, final ExecutablePluginSupport.ExecutionControl control) { long start = System.currentTimeMillis(); // get only the users that did ratings since the last execution List<Integer> users = actionDAO.getUsers(config.getTenant(), config.getItemTypes(), lastRun); stats.setNoUsers(users.size()); final int TOTAL_STEPS = users.size(); int currentStep = 0; for (int userId : users) { if (control != null) control.updateProgress(String.format("Calculating deviations %d/%d", currentStep++, TOTAL_STEPS)); // for each of these users get all his ratings List<RatingVO<Integer, Integer>> ratings = actionDAO.getRatings(config.getTenant(), config.getItemTypes(), userId); stats.setNumberOfActionsConsidered(stats.getNumberOfActionsConsidered() + ratings.size()); // and use them to calculate the new deviations (old deviations, i.e. deviations that were already // generated in a prior run are already filtered by the strategy.) // moreover a proxy strategy merges the deviations with the deviations in the database (i.e. numerator // and denominator are already summed to the current value.) DeviationCalculationResult result = deviationCalculation.calculate(userId, ratings, lastRun); List<Deviation> deviations = result.getDeviations(); stats.setNoCreatedDeviations(stats.getNoCreatedDeviations() + result.getCreated()); stats.setNoModifiedDeviations(stats.getNoModifiedDeviations() + result.getModified()); if (changedItemIds != null) { for (Deviation deviation : deviations) { changedItemIds.add(new TenantItem(deviation.getItem1Id(), deviation.getItem1TypeId())); changedItemIds.add(new TenantItem(deviation.getItem2Id(), deviation.getItem1TypeId())); } } deviationDAO.insertDeviations(deviations); } if (logger.isDebugEnabled()) logger.debug("finishing deviations calculation"); // endUpdate hint to DAO, so that if the DAO is cached the cache has a chance to write through to the underlying // store deviationDAO.endUpdate(); stats.setDeviationDuration(System.currentTimeMillis() - start); } public void generateActions(SlopeOneIntegerConfiguration config, LogEntry lastRun, SlopeOneStats stats) { long start = System.currentTimeMillis(); if (logger.isDebugEnabled()) logger.debug("starting action generation"); actionDAO.generateActions(config.getTenant(), config.getItemTypes(), config.getActionType(), lastRun.getExecution()); stats.setActionDuration(System.currentTimeMillis() - start); } public void nonPersonalizedRecommendations(final SlopeOneIntegerConfiguration config, final SlopeOneStats stats, final Date execution, final Set<TenantItem> changedItemIds, final ExecutionControl control) { final long start = System.currentTimeMillis(); final int MAX_ITEMASSOCS = 50000; // atomic to support usage in changedItemIds.forEach(AnonymousClass) final AtomicInteger itemAssocCount = new AtomicInteger(0); final int TOTAL_STEPS = changedItemIds.size(); // atomic to support usage in changedItemIds.forEach(AnonymousClass) final AtomicInteger currentStep = new AtomicInteger(0); Integer maxRecsPerItem = config.getMaxRecsPerItem(); final List<ItemAssocVO<Integer,Integer>> itemAssocs = new ArrayList<ItemAssocVO<Integer,Integer>>( Math.min(changedItemIds.size() * (maxRecsPerItem != null ? maxRecsPerItem : 10), MAX_ITEMASSOCS)); for (TenantItem changedItem : changedItemIds) { if (control != null) control.updateProgress( String.format("Calculating non-personalized recommendations %d/%d", currentStep.getAndIncrement(), TOTAL_STEPS)); List<Deviation> deviations = deviationDAO.getDeviationsOrdered(config.getTenant(), changedItem.getItemTypeId(), changedItem.getItemId(), config.getMinRatedCount(), config.getMaxRecsPerItem()); for (Deviation deviation : deviations) { ItemAssocVO<Integer,Integer> assoc = new ItemAssocVO<Integer,Integer>(config.getTenant(), deviation.getItem1(), config.getAssocType(), deviation.getDeviation(), deviation.getItem2(), config.getSourceType(), config.getNonPersonalizedSourceInfo(), config.getViewType(), Boolean.TRUE, execution); itemAssocs.add(assoc); } if (itemAssocs.size() >= MAX_ITEMASSOCS) { itemAssocCount.getAndAdd((itemAssocs.size())); itemAssocDAO.insertOrUpdateItemAssocs(itemAssocs); itemAssocs.clear(); } } if (itemAssocs.size() > 0) { itemAssocCount.getAndAdd((itemAssocs.size())); itemAssocDAO.insertOrUpdateItemAssocs(itemAssocs); itemAssocs.clear(); } itemAssocDAO.removeItemAssocByTenant(config.getTenant(), config.getAssocType(), config.getSourceType(), execution); stats.setNumberOfRulesCreated(itemAssocCount.get()); stats.setNonPersonalizedDuration(System.currentTimeMillis() - start); } @SuppressWarnings({"UnusedDeclaration"}) public void personalizedRecommendations(TenantVO tenant, SlopeOneIntegerConfiguration config, SlopeOneStats stats, Date execution, Set<TenantItem> changedItemIds, boolean weighted, final ExecutionControl control) { List<Integer> userIds = actionDAO.getUsers(config.getTenant(), config.getItemTypes(), execution); final int TOTAL_STEPS = userIds.size(); int currentStep = 0; for (int userId : userIds) { if (control != null) control.updateProgress( String.format("Calculating personalized recommendations %d/%d", currentStep++, TOTAL_STEPS)); List<RatingVO<Integer, Integer>> ratings = actionDAO.getRatings( config.getTenant(), config.getItemTypes(), userId); for (RatingVO<Integer, Integer> rating : ratings) { int itemId = rating.getItem().getItem(); int itemTypeId = rating.getItem().getType(); if (!changedItemIds.contains(new TenantItem(itemId, itemTypeId))) continue; List<Deviation> deviations = deviationDAO.getDeviationsOrdered(config.getTenant(), itemTypeId, itemId, config.getMinRatedCount(), config.getMaxRecsPerItem()); double recommendation = weighted ? weightedRecommendations(rating, deviations) : plainRecommendations(rating, deviations); if (logger.isDebugEnabled()) logger.debug("created recommendation " + recommendation); // TODO write to database } } } private double weightedRecommendations(final RatingVO<Integer, Integer> rating, final List<Deviation> deviations) { double numerator = 0.0; long denominator = 0L; int itemId = rating.getItem().getItem(); for (Deviation deviation : deviations) { if (deviation.getItem2Id() == itemId) continue; numerator += (deviation.getDeviation() * rating.getRatingValue()) * deviation.getDenominator(); denominator += deviation.getDenominator(); } return numerator / denominator; } private double plainRecommendations(final RatingVO<Integer, Integer> rating, final List<Deviation> deviations) { double numerator = 0.0; long denominator = 0L; int itemId = rating.getItem().getItem(); for (Deviation deviation : deviations) { if (deviation.getItem2Id() == itemId) continue; numerator += deviation.getDeviation() * rating.getRatingValue(); denominator++; } return numerator / denominator; } }