http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/AbstractJDBCIDMigrator.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/AbstractJDBCIDMigrator.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/AbstractJDBCIDMigrator.java new file mode 100644 index 0000000..cd3a434 --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/AbstractJDBCIDMigrator.java @@ -0,0 +1,108 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +import javax.sql.DataSource; + +import org.apache.mahout.cf.taste.common.TasteException; +import org.apache.mahout.cf.taste.model.UpdatableIDMigrator; +import org.apache.mahout.common.IOUtils; + +/** + * Implementation which stores the reverse long-to-String mapping in a database. Subclasses can override and + * configure the class to operate with particular databases by supplying appropriate SQL statements to the + * constructor. + */ +public abstract class AbstractJDBCIDMigrator extends AbstractIDMigrator implements UpdatableIDMigrator { + + public static final String DEFAULT_MAPPING_TABLE = "taste_id_mapping"; + public static final String DEFAULT_LONG_ID_COLUMN = "long_id"; + public static final String DEFAULT_STRING_ID_COLUMN = "string_id"; + + private final DataSource dataSource; + private final String getStringIDSQL; + private final String storeMappingSQL; + + /** + * @param getStringIDSQL + * SQL statement which selects one column, the String ID, from a mapping table. The statement + * should take one long parameter. + * @param storeMappingSQL + * SQL statement which saves a mapping from long to String. It should take two parameters, a long + * and a String. + */ + protected AbstractJDBCIDMigrator(DataSource dataSource, String getStringIDSQL, String storeMappingSQL) { + this.dataSource = dataSource; + this.getStringIDSQL = getStringIDSQL; + this.storeMappingSQL = storeMappingSQL; + } + + @Override + public final void storeMapping(long longID, String stringID) throws TasteException { + Connection conn = null; + PreparedStatement stmt = null; + try { + conn = dataSource.getConnection(); + stmt = conn.prepareStatement(storeMappingSQL); + stmt.setLong(1, longID); + stmt.setString(2, stringID); + stmt.executeUpdate(); + } catch (SQLException sqle) { + throw new TasteException(sqle); + } finally { + IOUtils.quietClose(null, stmt, conn); + } + } + + @Override + public final String toStringID(long longID) throws TasteException { + Connection conn = null; + PreparedStatement stmt = null; + ResultSet rs = null; + try { + conn = dataSource.getConnection(); + stmt = conn.prepareStatement(getStringIDSQL, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); + stmt.setFetchDirection(ResultSet.FETCH_FORWARD); + stmt.setFetchSize(1); + stmt.setLong(1, longID); + rs = stmt.executeQuery(); + if (rs.next()) { + return rs.getString(1); + } else { + return null; + } + } catch (SQLException sqle) { + throw new TasteException(sqle); + } finally { + IOUtils.quietClose(rs, stmt, conn); + } + } + + @Override + public void initialize(Iterable<String> stringIDs) throws TasteException { + for (String stringID : stringIDs) { + storeMapping(toLongID(stringID), stringID); + } + } + +}
http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanItemPreferenceArray.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanItemPreferenceArray.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanItemPreferenceArray.java new file mode 100644 index 0000000..6db5807 --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanItemPreferenceArray.java @@ -0,0 +1,234 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import com.google.common.base.Function; +import com.google.common.collect.Iterators; +import org.apache.mahout.cf.taste.model.Preference; +import org.apache.mahout.cf.taste.model.PreferenceArray; +import org.apache.mahout.common.iterator.CountingIterator; + +/** + * <p> + * Like {@link BooleanUserPreferenceArray} but stores preferences for one item (all item IDs the same) rather + * than one user. + * </p> + * + * @see BooleanPreference + * @see BooleanUserPreferenceArray + * @see GenericItemPreferenceArray + */ +public final class BooleanItemPreferenceArray implements PreferenceArray { + + private final long[] ids; + private long id; + + public BooleanItemPreferenceArray(int size) { + this.ids = new long[size]; + this.id = Long.MIN_VALUE; // as a sort of 'unspecified' value + } + + public BooleanItemPreferenceArray(List<? extends Preference> prefs, boolean forOneUser) { + this(prefs.size()); + int size = prefs.size(); + for (int i = 0; i < size; i++) { + Preference pref = prefs.get(i); + ids[i] = forOneUser ? pref.getItemID() : pref.getUserID(); + } + if (size > 0) { + id = forOneUser ? prefs.get(0).getUserID() : prefs.get(0).getItemID(); + } + } + + /** + * This is a private copy constructor for clone(). + */ + private BooleanItemPreferenceArray(long[] ids, long id) { + this.ids = ids; + this.id = id; + } + + @Override + public int length() { + return ids.length; + } + + @Override + public Preference get(int i) { + return new PreferenceView(i); + } + + @Override + public void set(int i, Preference pref) { + id = pref.getItemID(); + ids[i] = pref.getUserID(); + } + + @Override + public long getUserID(int i) { + return ids[i]; + } + + @Override + public void setUserID(int i, long userID) { + ids[i] = userID; + } + + @Override + public long getItemID(int i) { + return id; + } + + /** + * {@inheritDoc} + * + * Note that this method will actually set the item ID for <em>all</em> preferences. + */ + @Override + public void setItemID(int i, long itemID) { + id = itemID; + } + + /** + * @return all user IDs + */ + @Override + public long[] getIDs() { + return ids; + } + + @Override + public float getValue(int i) { + return 1.0f; + } + + @Override + public void setValue(int i, float value) { + throw new UnsupportedOperationException(); + } + + @Override + public void sortByUser() { + Arrays.sort(ids); + } + + @Override + public void sortByItem() { } + + @Override + public void sortByValue() { } + + @Override + public void sortByValueReversed() { } + + @Override + public boolean hasPrefWithUserID(long userID) { + for (long id : ids) { + if (userID == id) { + return true; + } + } + return false; + } + + @Override + public boolean hasPrefWithItemID(long itemID) { + return id == itemID; + } + + @Override + public BooleanItemPreferenceArray clone() { + return new BooleanItemPreferenceArray(ids.clone(), id); + } + + @Override + public int hashCode() { + return (int) (id >> 32) ^ (int) id ^ Arrays.hashCode(ids); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof BooleanItemPreferenceArray)) { + return false; + } + BooleanItemPreferenceArray otherArray = (BooleanItemPreferenceArray) other; + return id == otherArray.id && Arrays.equals(ids, otherArray.ids); + } + + @Override + public Iterator<Preference> iterator() { + return Iterators.transform(new CountingIterator(length()), + new Function<Integer, Preference>() { + @Override + public Preference apply(Integer from) { + return new PreferenceView(from); + } + }); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(10 * ids.length); + result.append("BooleanItemPreferenceArray[itemID:"); + result.append(id); + result.append(",{"); + for (int i = 0; i < ids.length; i++) { + if (i > 0) { + result.append(','); + } + result.append(ids[i]); + } + result.append("}]"); + return result.toString(); + } + + private final class PreferenceView implements Preference { + + private final int i; + + private PreferenceView(int i) { + this.i = i; + } + + @Override + public long getUserID() { + return BooleanItemPreferenceArray.this.getUserID(i); + } + + @Override + public long getItemID() { + return BooleanItemPreferenceArray.this.getItemID(i); + } + + @Override + public float getValue() { + return 1.0f; + } + + @Override + public void setValue(float value) { + throw new UnsupportedOperationException(); + } + + } + +} http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanPreference.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanPreference.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanPreference.java new file mode 100644 index 0000000..2093af8 --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanPreference.java @@ -0,0 +1,64 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import java.io.Serializable; + +import org.apache.mahout.cf.taste.model.Preference; + +/** + * Encapsulates a simple boolean "preference" for an item whose value does not matter (is fixed at 1.0). This + * is appropriate in situations where users conceptually have only a general "yes" preference for items, + * rather than a spectrum of preference values. + */ +public final class BooleanPreference implements Preference, Serializable { + + private final long userID; + private final long itemID; + + public BooleanPreference(long userID, long itemID) { + this.userID = userID; + this.itemID = itemID; + } + + @Override + public long getUserID() { + return userID; + } + + @Override + public long getItemID() { + return itemID; + } + + @Override + public float getValue() { + return 1.0f; + } + + @Override + public void setValue(float value) { + throw new UnsupportedOperationException(); + } + + @Override + public String toString() { + return "BooleanPreference[userID: " + userID + ", itemID:" + itemID + ']'; + } + +} http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanUserPreferenceArray.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanUserPreferenceArray.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanUserPreferenceArray.java new file mode 100644 index 0000000..629e0cf --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/BooleanUserPreferenceArray.java @@ -0,0 +1,234 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import com.google.common.base.Function; +import com.google.common.collect.Iterators; +import org.apache.mahout.cf.taste.model.Preference; +import org.apache.mahout.cf.taste.model.PreferenceArray; +import org.apache.mahout.common.iterator.CountingIterator; + +/** + * <p> + * Like {@link GenericUserPreferenceArray} but stores, conceptually, {@link BooleanPreference} objects which + * have no associated preference value. + * </p> + * + * @see BooleanPreference + * @see BooleanItemPreferenceArray + * @see GenericUserPreferenceArray + */ +public final class BooleanUserPreferenceArray implements PreferenceArray { + + private final long[] ids; + private long id; + + public BooleanUserPreferenceArray(int size) { + this.ids = new long[size]; + this.id = Long.MIN_VALUE; // as a sort of 'unspecified' value + } + + public BooleanUserPreferenceArray(List<? extends Preference> prefs) { + this(prefs.size()); + int size = prefs.size(); + for (int i = 0; i < size; i++) { + Preference pref = prefs.get(i); + ids[i] = pref.getItemID(); + } + if (size > 0) { + id = prefs.get(0).getUserID(); + } + } + + /** + * This is a private copy constructor for clone(). + */ + private BooleanUserPreferenceArray(long[] ids, long id) { + this.ids = ids; + this.id = id; + } + + @Override + public int length() { + return ids.length; + } + + @Override + public Preference get(int i) { + return new PreferenceView(i); + } + + @Override + public void set(int i, Preference pref) { + id = pref.getUserID(); + ids[i] = pref.getItemID(); + } + + @Override + public long getUserID(int i) { + return id; + } + + /** + * {@inheritDoc} + * + * Note that this method will actually set the user ID for <em>all</em> preferences. + */ + @Override + public void setUserID(int i, long userID) { + id = userID; + } + + @Override + public long getItemID(int i) { + return ids[i]; + } + + @Override + public void setItemID(int i, long itemID) { + ids[i] = itemID; + } + + /** + * @return all item IDs + */ + @Override + public long[] getIDs() { + return ids; + } + + @Override + public float getValue(int i) { + return 1.0f; + } + + @Override + public void setValue(int i, float value) { + throw new UnsupportedOperationException(); + } + + @Override + public void sortByUser() { } + + @Override + public void sortByItem() { + Arrays.sort(ids); + } + + @Override + public void sortByValue() { } + + @Override + public void sortByValueReversed() { } + + @Override + public boolean hasPrefWithUserID(long userID) { + return id == userID; + } + + @Override + public boolean hasPrefWithItemID(long itemID) { + for (long id : ids) { + if (itemID == id) { + return true; + } + } + return false; + } + + @Override + public BooleanUserPreferenceArray clone() { + return new BooleanUserPreferenceArray(ids.clone(), id); + } + + @Override + public int hashCode() { + return (int) (id >> 32) ^ (int) id ^ Arrays.hashCode(ids); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof BooleanUserPreferenceArray)) { + return false; + } + BooleanUserPreferenceArray otherArray = (BooleanUserPreferenceArray) other; + return id == otherArray.id && Arrays.equals(ids, otherArray.ids); + } + + @Override + public Iterator<Preference> iterator() { + return Iterators.transform(new CountingIterator(length()), + new Function<Integer, Preference>() { + @Override + public Preference apply(Integer from) { + return new PreferenceView(from); + } + }); + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(10 * ids.length); + result.append("BooleanUserPreferenceArray[userID:"); + result.append(id); + result.append(",{"); + for (int i = 0; i < ids.length; i++) { + if (i > 0) { + result.append(','); + } + result.append(ids[i]); + } + result.append("}]"); + return result.toString(); + } + + private final class PreferenceView implements Preference { + + private final int i; + + private PreferenceView(int i) { + this.i = i; + } + + @Override + public long getUserID() { + return BooleanUserPreferenceArray.this.getUserID(i); + } + + @Override + public long getItemID() { + return BooleanUserPreferenceArray.this.getItemID(i); + } + + @Override + public float getValue() { + return 1.0f; + } + + @Override + public void setValue(float value) { + throw new UnsupportedOperationException(); + } + + } + +} http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericBooleanPrefDataModel.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericBooleanPrefDataModel.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericBooleanPrefDataModel.java new file mode 100644 index 0000000..2c1ff4d --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericBooleanPrefDataModel.java @@ -0,0 +1,320 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Map; + +import org.apache.mahout.cf.taste.common.NoSuchItemException; +import org.apache.mahout.cf.taste.common.NoSuchUserException; +import org.apache.mahout.cf.taste.common.Refreshable; +import org.apache.mahout.cf.taste.common.TasteException; +import org.apache.mahout.cf.taste.impl.common.FastByIDMap; +import org.apache.mahout.cf.taste.impl.common.FastIDSet; +import org.apache.mahout.cf.taste.impl.common.LongPrimitiveArrayIterator; +import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator; +import org.apache.mahout.cf.taste.model.DataModel; +import org.apache.mahout.cf.taste.model.PreferenceArray; + +import com.google.common.base.Preconditions; + +/** + * <p> + * A simple {@link DataModel} which uses given user data as its data source. This implementation + * is mostly useful for small experiments and is not recommended for contexts where performance is important. + * </p> + */ +public final class GenericBooleanPrefDataModel extends AbstractDataModel { + + private final long[] userIDs; + private final FastByIDMap<FastIDSet> preferenceFromUsers; + private final long[] itemIDs; + private final FastByIDMap<FastIDSet> preferenceForItems; + private final FastByIDMap<FastByIDMap<Long>> timestamps; + + /** + * <p> + * Creates a new {@link GenericDataModel} from the given users (and their preferences). This + * {@link DataModel} retains all this information in memory and is effectively immutable. + * </p> + * + * @param userData users to include + */ + public GenericBooleanPrefDataModel(FastByIDMap<FastIDSet> userData) { + this(userData, null); + } + + /** + * <p> + * Creates a new {@link GenericDataModel} from the given users (and their preferences). This + * {@link DataModel} retains all this information in memory and is effectively immutable. + * </p> + * + * @param userData users to include + * @param timestamps optionally, provided timestamps of preferences as milliseconds since the epoch. + * User IDs are mapped to maps of item IDs to Long timestamps. + */ + public GenericBooleanPrefDataModel(FastByIDMap<FastIDSet> userData, FastByIDMap<FastByIDMap<Long>> timestamps) { + Preconditions.checkArgument(userData != null, "userData is null"); + + this.preferenceFromUsers = userData; + this.preferenceForItems = new FastByIDMap<>(); + FastIDSet itemIDSet = new FastIDSet(); + for (Map.Entry<Long, FastIDSet> entry : preferenceFromUsers.entrySet()) { + long userID = entry.getKey(); + FastIDSet itemIDs = entry.getValue(); + itemIDSet.addAll(itemIDs); + LongPrimitiveIterator it = itemIDs.iterator(); + while (it.hasNext()) { + long itemID = it.nextLong(); + FastIDSet userIDs = preferenceForItems.get(itemID); + if (userIDs == null) { + userIDs = new FastIDSet(2); + preferenceForItems.put(itemID, userIDs); + } + userIDs.add(userID); + } + } + + this.itemIDs = itemIDSet.toArray(); + itemIDSet = null; // Might help GC -- this is big + Arrays.sort(itemIDs); + + this.userIDs = new long[userData.size()]; + int i = 0; + LongPrimitiveIterator it = userData.keySetIterator(); + while (it.hasNext()) { + userIDs[i++] = it.next(); + } + Arrays.sort(userIDs); + + this.timestamps = timestamps; + } + + /** + * <p> + * Creates a new {@link GenericDataModel} containing an immutable copy of the data from another given + * {@link DataModel}. + * </p> + * + * @param dataModel + * {@link DataModel} to copy + * @throws TasteException + * if an error occurs while retrieving the other {@link DataModel}'s users + * @deprecated without direct replacement. + * Consider {@link #toDataMap(DataModel)} with {@link #GenericBooleanPrefDataModel(FastByIDMap)} + */ + @Deprecated + public GenericBooleanPrefDataModel(DataModel dataModel) throws TasteException { + this(toDataMap(dataModel)); + } + + /** + * Exports the simple user IDs and associated item IDs in the data model. + * + * @return a {@link FastByIDMap} mapping user IDs to {@link FastIDSet}s representing + * that user's associated items + */ + public static FastByIDMap<FastIDSet> toDataMap(DataModel dataModel) throws TasteException { + FastByIDMap<FastIDSet> data = new FastByIDMap<>(dataModel.getNumUsers()); + LongPrimitiveIterator it = dataModel.getUserIDs(); + while (it.hasNext()) { + long userID = it.nextLong(); + data.put(userID, dataModel.getItemIDsFromUser(userID)); + } + return data; + } + + public static FastByIDMap<FastIDSet> toDataMap(FastByIDMap<PreferenceArray> data) { + for (Map.Entry<Long,Object> entry : ((FastByIDMap<Object>) (FastByIDMap<?>) data).entrySet()) { + PreferenceArray prefArray = (PreferenceArray) entry.getValue(); + int size = prefArray.length(); + FastIDSet itemIDs = new FastIDSet(size); + for (int i = 0; i < size; i++) { + itemIDs.add(prefArray.getItemID(i)); + } + entry.setValue(itemIDs); + } + return (FastByIDMap<FastIDSet>) (FastByIDMap<?>) data; + } + + /** + * This is used mostly internally to the framework, and shouldn't be relied upon otherwise. + */ + public FastByIDMap<FastIDSet> getRawUserData() { + return this.preferenceFromUsers; + } + + /** + * This is used mostly internally to the framework, and shouldn't be relied upon otherwise. + */ + public FastByIDMap<FastIDSet> getRawItemData() { + return this.preferenceForItems; + } + + @Override + public LongPrimitiveArrayIterator getUserIDs() { + return new LongPrimitiveArrayIterator(userIDs); + } + + /** + * @throws NoSuchUserException + * if there is no such user + */ + @Override + public PreferenceArray getPreferencesFromUser(long userID) throws NoSuchUserException { + FastIDSet itemIDs = preferenceFromUsers.get(userID); + if (itemIDs == null) { + throw new NoSuchUserException(userID); + } + PreferenceArray prefArray = new BooleanUserPreferenceArray(itemIDs.size()); + int i = 0; + LongPrimitiveIterator it = itemIDs.iterator(); + while (it.hasNext()) { + prefArray.setUserID(i, userID); + prefArray.setItemID(i, it.nextLong()); + i++; + } + return prefArray; + } + + @Override + public FastIDSet getItemIDsFromUser(long userID) throws TasteException { + FastIDSet itemIDs = preferenceFromUsers.get(userID); + if (itemIDs == null) { + throw new NoSuchUserException(userID); + } + return itemIDs; + } + + @Override + public LongPrimitiveArrayIterator getItemIDs() { + return new LongPrimitiveArrayIterator(itemIDs); + } + + @Override + public PreferenceArray getPreferencesForItem(long itemID) throws NoSuchItemException { + FastIDSet userIDs = preferenceForItems.get(itemID); + if (userIDs == null) { + throw new NoSuchItemException(itemID); + } + PreferenceArray prefArray = new BooleanItemPreferenceArray(userIDs.size()); + int i = 0; + LongPrimitiveIterator it = userIDs.iterator(); + while (it.hasNext()) { + prefArray.setUserID(i, it.nextLong()); + prefArray.setItemID(i, itemID); + i++; + } + return prefArray; + } + + @Override + public Float getPreferenceValue(long userID, long itemID) throws NoSuchUserException { + FastIDSet itemIDs = preferenceFromUsers.get(userID); + if (itemIDs == null) { + throw new NoSuchUserException(userID); + } + if (itemIDs.contains(itemID)) { + return 1.0f; + } + return null; + } + + @Override + public Long getPreferenceTime(long userID, long itemID) throws TasteException { + if (timestamps == null) { + return null; + } + FastByIDMap<Long> itemTimestamps = timestamps.get(userID); + if (itemTimestamps == null) { + throw new NoSuchUserException(userID); + } + return itemTimestamps.get(itemID); + } + + @Override + public int getNumItems() { + return itemIDs.length; + } + + @Override + public int getNumUsers() { + return userIDs.length; + } + + @Override + public int getNumUsersWithPreferenceFor(long itemID) { + FastIDSet userIDs1 = preferenceForItems.get(itemID); + return userIDs1 == null ? 0 : userIDs1.size(); + } + + @Override + public int getNumUsersWithPreferenceFor(long itemID1, long itemID2) { + FastIDSet userIDs1 = preferenceForItems.get(itemID1); + if (userIDs1 == null) { + return 0; + } + FastIDSet userIDs2 = preferenceForItems.get(itemID2); + if (userIDs2 == null) { + return 0; + } + return userIDs1.size() < userIDs2.size() + ? userIDs2.intersectionSize(userIDs1) + : userIDs1.intersectionSize(userIDs2); + } + + @Override + public void removePreference(long userID, long itemID) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPreference(long userID, long itemID, float value) { + throw new UnsupportedOperationException(); + } + + @Override + public void refresh(Collection<Refreshable> alreadyRefreshed) { + // Does nothing + } + + @Override + public boolean hasPreferenceValues() { + return false; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(200); + result.append("GenericBooleanPrefDataModel[users:"); + for (int i = 0; i < Math.min(3, userIDs.length); i++) { + if (i > 0) { + result.append(','); + } + result.append(userIDs[i]); + } + if (userIDs.length > 3) { + result.append("..."); + } + result.append(']'); + return result.toString(); + } + +} http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericDataModel.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericDataModel.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericDataModel.java new file mode 100644 index 0000000..f58d349 --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericDataModel.java @@ -0,0 +1,361 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import com.google.common.collect.Lists; +import org.apache.mahout.cf.taste.common.NoSuchItemException; +import org.apache.mahout.cf.taste.common.NoSuchUserException; +import org.apache.mahout.cf.taste.common.Refreshable; +import org.apache.mahout.cf.taste.common.TasteException; +import org.apache.mahout.cf.taste.impl.common.FastByIDMap; +import org.apache.mahout.cf.taste.impl.common.FastIDSet; +import org.apache.mahout.cf.taste.impl.common.LongPrimitiveArrayIterator; +import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator; +import org.apache.mahout.cf.taste.model.DataModel; +import org.apache.mahout.cf.taste.model.Preference; +import org.apache.mahout.cf.taste.model.PreferenceArray; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Preconditions; + +/** + * <p> + * A simple {@link DataModel} which uses a given {@link List} of users as its data source. This implementation + * is mostly useful for small experiments and is not recommended for contexts where performance is important. + * </p> + */ +public final class GenericDataModel extends AbstractDataModel { + + private static final Logger log = LoggerFactory.getLogger(GenericDataModel.class); + + private final long[] userIDs; + private final FastByIDMap<PreferenceArray> preferenceFromUsers; + private final long[] itemIDs; + private final FastByIDMap<PreferenceArray> preferenceForItems; + private final FastByIDMap<FastByIDMap<Long>> timestamps; + + /** + * <p> + * Creates a new {@link GenericDataModel} from the given users (and their preferences). This + * {@link DataModel} retains all this information in memory and is effectively immutable. + * </p> + * + * @param userData users to include; (see also {@link #toDataMap(FastByIDMap, boolean)}) + */ + public GenericDataModel(FastByIDMap<PreferenceArray> userData) { + this(userData, null); + } + + /** + * <p> + * Creates a new {@link GenericDataModel} from the given users (and their preferences). This + * {@link DataModel} retains all this information in memory and is effectively immutable. + * </p> + * + * @param userData users to include; (see also {@link #toDataMap(FastByIDMap, boolean)}) + * @param timestamps optionally, provided timestamps of preferences as milliseconds since the epoch. + * User IDs are mapped to maps of item IDs to Long timestamps. + */ + public GenericDataModel(FastByIDMap<PreferenceArray> userData, FastByIDMap<FastByIDMap<Long>> timestamps) { + Preconditions.checkArgument(userData != null, "userData is null"); + + this.preferenceFromUsers = userData; + FastByIDMap<Collection<Preference>> prefsForItems = new FastByIDMap<>(); + FastIDSet itemIDSet = new FastIDSet(); + int currentCount = 0; + float maxPrefValue = Float.NEGATIVE_INFINITY; + float minPrefValue = Float.POSITIVE_INFINITY; + for (Map.Entry<Long, PreferenceArray> entry : preferenceFromUsers.entrySet()) { + PreferenceArray prefs = entry.getValue(); + prefs.sortByItem(); + for (Preference preference : prefs) { + long itemID = preference.getItemID(); + itemIDSet.add(itemID); + Collection<Preference> prefsForItem = prefsForItems.get(itemID); + if (prefsForItem == null) { + prefsForItem = Lists.newArrayListWithCapacity(2); + prefsForItems.put(itemID, prefsForItem); + } + prefsForItem.add(preference); + float value = preference.getValue(); + if (value > maxPrefValue) { + maxPrefValue = value; + } + if (value < minPrefValue) { + minPrefValue = value; + } + } + if (++currentCount % 10000 == 0) { + log.info("Processed {} users", currentCount); + } + } + log.info("Processed {} users", currentCount); + + setMinPreference(minPrefValue); + setMaxPreference(maxPrefValue); + + this.itemIDs = itemIDSet.toArray(); + itemIDSet = null; // Might help GC -- this is big + Arrays.sort(itemIDs); + + this.preferenceForItems = toDataMap(prefsForItems, false); + + for (Map.Entry<Long, PreferenceArray> entry : preferenceForItems.entrySet()) { + entry.getValue().sortByUser(); + } + + this.userIDs = new long[userData.size()]; + int i = 0; + LongPrimitiveIterator it = userData.keySetIterator(); + while (it.hasNext()) { + userIDs[i++] = it.next(); + } + Arrays.sort(userIDs); + + this.timestamps = timestamps; + } + + /** + * <p> + * Creates a new {@link GenericDataModel} containing an immutable copy of the data from another given + * {@link DataModel}. + * </p> + * + * @param dataModel {@link DataModel} to copy + * @throws TasteException if an error occurs while retrieving the other {@link DataModel}'s users + * @deprecated without direct replacement. + * Consider {@link #toDataMap(DataModel)} with {@link #GenericDataModel(FastByIDMap)} + */ + @Deprecated + public GenericDataModel(DataModel dataModel) throws TasteException { + this(toDataMap(dataModel)); + } + + /** + * Swaps, in-place, {@link List}s for arrays in {@link Map} values . + * + * @return input value + */ + public static FastByIDMap<PreferenceArray> toDataMap(FastByIDMap<Collection<Preference>> data, + boolean byUser) { + for (Map.Entry<Long,Object> entry : ((FastByIDMap<Object>) (FastByIDMap<?>) data).entrySet()) { + List<Preference> prefList = (List<Preference>) entry.getValue(); + entry.setValue(byUser ? new GenericUserPreferenceArray(prefList) : new GenericItemPreferenceArray( + prefList)); + } + return (FastByIDMap<PreferenceArray>) (FastByIDMap<?>) data; + } + + /** + * Exports the simple user IDs and preferences in the data model. + * + * @return a {@link FastByIDMap} mapping user IDs to {@link PreferenceArray}s representing + * that user's preferences + */ + public static FastByIDMap<PreferenceArray> toDataMap(DataModel dataModel) throws TasteException { + FastByIDMap<PreferenceArray> data = new FastByIDMap<>(dataModel.getNumUsers()); + LongPrimitiveIterator it = dataModel.getUserIDs(); + while (it.hasNext()) { + long userID = it.nextLong(); + data.put(userID, dataModel.getPreferencesFromUser(userID)); + } + return data; + } + + /** + * This is used mostly internally to the framework, and shouldn't be relied upon otherwise. + */ + public FastByIDMap<PreferenceArray> getRawUserData() { + return this.preferenceFromUsers; + } + + /** + * This is used mostly internally to the framework, and shouldn't be relied upon otherwise. + */ + public FastByIDMap<PreferenceArray> getRawItemData() { + return this.preferenceForItems; + } + + @Override + public LongPrimitiveArrayIterator getUserIDs() { + return new LongPrimitiveArrayIterator(userIDs); + } + + /** + * @throws NoSuchUserException + * if there is no such user + */ + @Override + public PreferenceArray getPreferencesFromUser(long userID) throws NoSuchUserException { + PreferenceArray prefs = preferenceFromUsers.get(userID); + if (prefs == null) { + throw new NoSuchUserException(userID); + } + return prefs; + } + + @Override + public FastIDSet getItemIDsFromUser(long userID) throws TasteException { + PreferenceArray prefs = getPreferencesFromUser(userID); + int size = prefs.length(); + FastIDSet result = new FastIDSet(size); + for (int i = 0; i < size; i++) { + result.add(prefs.getItemID(i)); + } + return result; + } + + @Override + public LongPrimitiveArrayIterator getItemIDs() { + return new LongPrimitiveArrayIterator(itemIDs); + } + + @Override + public PreferenceArray getPreferencesForItem(long itemID) throws NoSuchItemException { + PreferenceArray prefs = preferenceForItems.get(itemID); + if (prefs == null) { + throw new NoSuchItemException(itemID); + } + return prefs; + } + + @Override + public Float getPreferenceValue(long userID, long itemID) throws TasteException { + PreferenceArray prefs = getPreferencesFromUser(userID); + int size = prefs.length(); + for (int i = 0; i < size; i++) { + if (prefs.getItemID(i) == itemID) { + return prefs.getValue(i); + } + } + return null; + } + + @Override + public Long getPreferenceTime(long userID, long itemID) throws TasteException { + if (timestamps == null) { + return null; + } + FastByIDMap<Long> itemTimestamps = timestamps.get(userID); + if (itemTimestamps == null) { + throw new NoSuchUserException(userID); + } + return itemTimestamps.get(itemID); + } + + @Override + public int getNumItems() { + return itemIDs.length; + } + + @Override + public int getNumUsers() { + return userIDs.length; + } + + @Override + public int getNumUsersWithPreferenceFor(long itemID) { + PreferenceArray prefs1 = preferenceForItems.get(itemID); + return prefs1 == null ? 0 : prefs1.length(); + } + + @Override + public int getNumUsersWithPreferenceFor(long itemID1, long itemID2) { + PreferenceArray prefs1 = preferenceForItems.get(itemID1); + if (prefs1 == null) { + return 0; + } + PreferenceArray prefs2 = preferenceForItems.get(itemID2); + if (prefs2 == null) { + return 0; + } + + int size1 = prefs1.length(); + int size2 = prefs2.length(); + int count = 0; + int i = 0; + int j = 0; + long userID1 = prefs1.getUserID(0); + long userID2 = prefs2.getUserID(0); + while (true) { + if (userID1 < userID2) { + if (++i == size1) { + break; + } + userID1 = prefs1.getUserID(i); + } else if (userID1 > userID2) { + if (++j == size2) { + break; + } + userID2 = prefs2.getUserID(j); + } else { + count++; + if (++i == size1 || ++j == size2) { + break; + } + userID1 = prefs1.getUserID(i); + userID2 = prefs2.getUserID(j); + } + } + return count; + } + + @Override + public void removePreference(long userID, long itemID) { + throw new UnsupportedOperationException(); + } + + @Override + public void setPreference(long userID, long itemID, float value) { + throw new UnsupportedOperationException(); + } + + @Override + public void refresh(Collection<Refreshable> alreadyRefreshed) { + // Does nothing + } + + @Override + public boolean hasPreferenceValues() { + return true; + } + + @Override + public String toString() { + StringBuilder result = new StringBuilder(200); + result.append("GenericDataModel[users:"); + for (int i = 0; i < Math.min(3, userIDs.length); i++) { + if (i > 0) { + result.append(','); + } + result.append(userIDs[i]); + } + if (userIDs.length > 3) { + result.append("..."); + } + result.append(']'); + return result.toString(); + } + +} http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericItemPreferenceArray.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericItemPreferenceArray.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericItemPreferenceArray.java new file mode 100644 index 0000000..fde9314 --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericItemPreferenceArray.java @@ -0,0 +1,301 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import com.google.common.base.Function; +import com.google.common.collect.Iterators; +import org.apache.mahout.cf.taste.model.Preference; +import org.apache.mahout.cf.taste.model.PreferenceArray; +import org.apache.mahout.common.iterator.CountingIterator; + +/** + * <p> + * Like {@link GenericUserPreferenceArray} but stores preferences for one item (all item IDs the same) rather + * than one user. + * </p> + * + * @see BooleanItemPreferenceArray + * @see GenericUserPreferenceArray + * @see GenericPreference + */ +public final class GenericItemPreferenceArray implements PreferenceArray { + + private static final int USER = 0; + private static final int VALUE = 2; + private static final int VALUE_REVERSED = 3; + + private final long[] ids; + private long id; + private final float[] values; + + public GenericItemPreferenceArray(int size) { + this.ids = new long[size]; + values = new float[size]; + this.id = Long.MIN_VALUE; // as a sort of 'unspecified' value + } + + public GenericItemPreferenceArray(List<? extends Preference> prefs) { + this(prefs.size()); + int size = prefs.size(); + long itemID = Long.MIN_VALUE; + for (int i = 0; i < size; i++) { + Preference pref = prefs.get(i); + ids[i] = pref.getUserID(); + if (i == 0) { + itemID = pref.getItemID(); + } else { + if (itemID != pref.getItemID()) { + throw new IllegalArgumentException("Not all item IDs are the same"); + } + } + values[i] = pref.getValue(); + } + id = itemID; + } + + /** + * This is a private copy constructor for clone(). + */ + private GenericItemPreferenceArray(long[] ids, long id, float[] values) { + this.ids = ids; + this.id = id; + this.values = values; + } + + @Override + public int length() { + return ids.length; + } + + @Override + public Preference get(int i) { + return new PreferenceView(i); + } + + @Override + public void set(int i, Preference pref) { + id = pref.getItemID(); + ids[i] = pref.getUserID(); + values[i] = pref.getValue(); + } + + @Override + public long getUserID(int i) { + return ids[i]; + } + + @Override + public void setUserID(int i, long userID) { + ids[i] = userID; + } + + @Override + public long getItemID(int i) { + return id; + } + + /** + * {@inheritDoc} + * + * Note that this method will actually set the item ID for <em>all</em> preferences. + */ + @Override + public void setItemID(int i, long itemID) { + id = itemID; + } + + /** + * @return all user IDs + */ + @Override + public long[] getIDs() { + return ids; + } + + @Override + public float getValue(int i) { + return values[i]; + } + + @Override + public void setValue(int i, float value) { + values[i] = value; + } + + @Override + public void sortByUser() { + lateralSort(USER); + } + + @Override + public void sortByItem() { } + + @Override + public void sortByValue() { + lateralSort(VALUE); + } + + @Override + public void sortByValueReversed() { + lateralSort(VALUE_REVERSED); + } + + @Override + public boolean hasPrefWithUserID(long userID) { + for (long id : ids) { + if (userID == id) { + return true; + } + } + return false; + } + + @Override + public boolean hasPrefWithItemID(long itemID) { + return id == itemID; + } + + private void lateralSort(int type) { + //Comb sort: http://en.wikipedia.org/wiki/Comb_sort + int length = length(); + int gap = length; + boolean swapped = false; + while (gap > 1 || swapped) { + if (gap > 1) { + gap /= 1.247330950103979; // = 1 / (1 - 1/e^phi) + } + swapped = false; + int max = length - gap; + for (int i = 0; i < max; i++) { + int other = i + gap; + if (isLess(other, i, type)) { + swap(i, other); + swapped = true; + } + } + } + } + + private boolean isLess(int i, int j, int type) { + switch (type) { + case USER: + return ids[i] < ids[j]; + case VALUE: + return values[i] < values[j]; + case VALUE_REVERSED: + return values[i] > values[j]; + default: + throw new IllegalStateException(); + } + } + + private void swap(int i, int j) { + long temp1 = ids[i]; + float temp2 = values[i]; + ids[i] = ids[j]; + values[i] = values[j]; + ids[j] = temp1; + values[j] = temp2; + } + + @Override + public GenericItemPreferenceArray clone() { + return new GenericItemPreferenceArray(ids.clone(), id, values.clone()); + } + + @Override + public int hashCode() { + return (int) (id >> 32) ^ (int) id ^ Arrays.hashCode(ids) ^ Arrays.hashCode(values); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof GenericItemPreferenceArray)) { + return false; + } + GenericItemPreferenceArray otherArray = (GenericItemPreferenceArray) other; + return id == otherArray.id && Arrays.equals(ids, otherArray.ids) && Arrays.equals(values, otherArray.values); + } + + @Override + public Iterator<Preference> iterator() { + return Iterators.transform(new CountingIterator(length()), + new Function<Integer, Preference>() { + @Override + public Preference apply(Integer from) { + return new PreferenceView(from); + } + }); + } + + @Override + public String toString() { + if (ids == null || ids.length == 0) { + return "GenericItemPreferenceArray[{}]"; + } + StringBuilder result = new StringBuilder(20 * ids.length); + result.append("GenericItemPreferenceArray[itemID:"); + result.append(id); + result.append(",{"); + for (int i = 0; i < ids.length; i++) { + if (i > 0) { + result.append(','); + } + result.append(ids[i]); + result.append('='); + result.append(values[i]); + } + result.append("}]"); + return result.toString(); + } + + private final class PreferenceView implements Preference { + + private final int i; + + private PreferenceView(int i) { + this.i = i; + } + + @Override + public long getUserID() { + return GenericItemPreferenceArray.this.getUserID(i); + } + + @Override + public long getItemID() { + return GenericItemPreferenceArray.this.getItemID(i); + } + + @Override + public float getValue() { + return values[i]; + } + + @Override + public void setValue(float value) { + values[i] = value; + } + + } + +} http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericPreference.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericPreference.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericPreference.java new file mode 100644 index 0000000..e6c7f43 --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericPreference.java @@ -0,0 +1,70 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import java.io.Serializable; + +import org.apache.mahout.cf.taste.model.Preference; + +import com.google.common.base.Preconditions; + +/** + * <p> + * A simple {@link Preference} encapsulating an item and preference value. + * </p> + */ +public class GenericPreference implements Preference, Serializable { + + private final long userID; + private final long itemID; + private float value; + + public GenericPreference(long userID, long itemID, float value) { + Preconditions.checkArgument(!Float.isNaN(value), "NaN value"); + this.userID = userID; + this.itemID = itemID; + this.value = value; + } + + @Override + public long getUserID() { + return userID; + } + + @Override + public long getItemID() { + return itemID; + } + + @Override + public float getValue() { + return value; + } + + @Override + public void setValue(float value) { + Preconditions.checkArgument(!Float.isNaN(value), "NaN value"); + this.value = value; + } + + @Override + public String toString() { + return "GenericPreference[userID: " + userID + ", itemID:" + itemID + ", value:" + value + ']'; + } + +} http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericUserPreferenceArray.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericUserPreferenceArray.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericUserPreferenceArray.java new file mode 100644 index 0000000..647feeb --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/GenericUserPreferenceArray.java @@ -0,0 +1,307 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; + +import com.google.common.base.Function; +import com.google.common.collect.Iterators; +import org.apache.mahout.cf.taste.model.Preference; +import org.apache.mahout.cf.taste.model.PreferenceArray; +import org.apache.mahout.common.iterator.CountingIterator; + +/** + * <p> + * Like {@link GenericItemPreferenceArray} but stores preferences for one user (all user IDs the same) rather + * than one item. + * </p> + * + * <p> + * This implementation maintains two parallel arrays, of item IDs and values. The idea is to save allocating + * {@link Preference} objects themselves. This saves the overhead of {@link Preference} objects but also + * duplicating the user ID value. + * </p> + * + * @see BooleanUserPreferenceArray + * @see GenericItemPreferenceArray + * @see GenericPreference + */ +public final class GenericUserPreferenceArray implements PreferenceArray { + + private static final int ITEM = 1; + private static final int VALUE = 2; + private static final int VALUE_REVERSED = 3; + + private final long[] ids; + private long id; + private final float[] values; + + public GenericUserPreferenceArray(int size) { + this.ids = new long[size]; + values = new float[size]; + this.id = Long.MIN_VALUE; // as a sort of 'unspecified' value + } + + public GenericUserPreferenceArray(List<? extends Preference> prefs) { + this(prefs.size()); + int size = prefs.size(); + long userID = Long.MIN_VALUE; + for (int i = 0; i < size; i++) { + Preference pref = prefs.get(i); + if (i == 0) { + userID = pref.getUserID(); + } else { + if (userID != pref.getUserID()) { + throw new IllegalArgumentException("Not all user IDs are the same"); + } + } + ids[i] = pref.getItemID(); + values[i] = pref.getValue(); + } + id = userID; + } + + /** + * This is a private copy constructor for clone(). + */ + private GenericUserPreferenceArray(long[] ids, long id, float[] values) { + this.ids = ids; + this.id = id; + this.values = values; + } + + @Override + public int length() { + return ids.length; + } + + @Override + public Preference get(int i) { + return new PreferenceView(i); + } + + @Override + public void set(int i, Preference pref) { + id = pref.getUserID(); + ids[i] = pref.getItemID(); + values[i] = pref.getValue(); + } + + @Override + public long getUserID(int i) { + return id; + } + + /** + * {@inheritDoc} + * + * Note that this method will actually set the user ID for <em>all</em> preferences. + */ + @Override + public void setUserID(int i, long userID) { + id = userID; + } + + @Override + public long getItemID(int i) { + return ids[i]; + } + + @Override + public void setItemID(int i, long itemID) { + ids[i] = itemID; + } + + /** + * @return all item IDs + */ + @Override + public long[] getIDs() { + return ids; + } + + @Override + public float getValue(int i) { + return values[i]; + } + + @Override + public void setValue(int i, float value) { + values[i] = value; + } + + @Override + public void sortByUser() { } + + @Override + public void sortByItem() { + lateralSort(ITEM); + } + + @Override + public void sortByValue() { + lateralSort(VALUE); + } + + @Override + public void sortByValueReversed() { + lateralSort(VALUE_REVERSED); + } + + @Override + public boolean hasPrefWithUserID(long userID) { + return id == userID; + } + + @Override + public boolean hasPrefWithItemID(long itemID) { + for (long id : ids) { + if (itemID == id) { + return true; + } + } + return false; + } + + private void lateralSort(int type) { + //Comb sort: http://en.wikipedia.org/wiki/Comb_sort + int length = length(); + int gap = length; + boolean swapped = false; + while (gap > 1 || swapped) { + if (gap > 1) { + gap /= 1.247330950103979; // = 1 / (1 - 1/e^phi) + } + swapped = false; + int max = length - gap; + for (int i = 0; i < max; i++) { + int other = i + gap; + if (isLess(other, i, type)) { + swap(i, other); + swapped = true; + } + } + } + } + + private boolean isLess(int i, int j, int type) { + switch (type) { + case ITEM: + return ids[i] < ids[j]; + case VALUE: + return values[i] < values[j]; + case VALUE_REVERSED: + return values[i] > values[j]; + default: + throw new IllegalStateException(); + } + } + + private void swap(int i, int j) { + long temp1 = ids[i]; + float temp2 = values[i]; + ids[i] = ids[j]; + values[i] = values[j]; + ids[j] = temp1; + values[j] = temp2; + } + + @Override + public GenericUserPreferenceArray clone() { + return new GenericUserPreferenceArray(ids.clone(), id, values.clone()); + } + + @Override + public int hashCode() { + return (int) (id >> 32) ^ (int) id ^ Arrays.hashCode(ids) ^ Arrays.hashCode(values); + } + + @Override + public boolean equals(Object other) { + if (!(other instanceof GenericUserPreferenceArray)) { + return false; + } + GenericUserPreferenceArray otherArray = (GenericUserPreferenceArray) other; + return id == otherArray.id && Arrays.equals(ids, otherArray.ids) && Arrays.equals(values, otherArray.values); + } + + @Override + public Iterator<Preference> iterator() { + return Iterators.transform(new CountingIterator(length()), + new Function<Integer, Preference>() { + @Override + public Preference apply(Integer from) { + return new PreferenceView(from); + } + }); + } + + @Override + public String toString() { + if (ids == null || ids.length == 0) { + return "GenericUserPreferenceArray[{}]"; + } + StringBuilder result = new StringBuilder(20 * ids.length); + result.append("GenericUserPreferenceArray[userID:"); + result.append(id); + result.append(",{"); + for (int i = 0; i < ids.length; i++) { + if (i > 0) { + result.append(','); + } + result.append(ids[i]); + result.append('='); + result.append(values[i]); + } + result.append("}]"); + return result.toString(); + } + + private final class PreferenceView implements Preference { + + private final int i; + + private PreferenceView(int i) { + this.i = i; + } + + @Override + public long getUserID() { + return GenericUserPreferenceArray.this.getUserID(i); + } + + @Override + public long getItemID() { + return GenericUserPreferenceArray.this.getItemID(i); + } + + @Override + public float getValue() { + return values[i]; + } + + @Override + public void setValue(float value) { + values[i] = value; + } + + } + +} http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/MemoryIDMigrator.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/MemoryIDMigrator.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/MemoryIDMigrator.java new file mode 100644 index 0000000..3463ff5 --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/MemoryIDMigrator.java @@ -0,0 +1,55 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import org.apache.mahout.cf.taste.impl.common.FastByIDMap; +import org.apache.mahout.cf.taste.model.UpdatableIDMigrator; + +/** + * Implementation which stores the reverse long-to-String mapping in memory. + */ +public final class MemoryIDMigrator extends AbstractIDMigrator implements UpdatableIDMigrator { + + private final FastByIDMap<String> longToString; + + public MemoryIDMigrator() { + this.longToString = new FastByIDMap<>(100); + } + + @Override + public void storeMapping(long longID, String stringID) { + synchronized (longToString) { + longToString.put(longID, stringID); + } + } + + @Override + public String toStringID(long longID) { + synchronized (longToString) { + return longToString.get(longID); + } + } + + @Override + public void initialize(Iterable<String> stringIDs) { + for (String stringID : stringIDs) { + storeMapping(toLongID(stringID), stringID); + } + } + +} http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/MySQLJDBCIDMigrator.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/MySQLJDBCIDMigrator.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/MySQLJDBCIDMigrator.java new file mode 100644 index 0000000..b134598 --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/MySQLJDBCIDMigrator.java @@ -0,0 +1,67 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import javax.sql.DataSource; + +/** + * <p> + * An implementation for MySQL. The following statement would create a table suitable for use with this class: + * </p> + * + * <p> + * + * <pre> + * CREATE TABLE taste_id_migration ( + * long_id BIGINT NOT NULL PRIMARY KEY, + * string_id VARCHAR(255) NOT NULL UNIQUE + * ) + * </pre> + * + * </p> + * + * <p> + * Separately, note that in a MySQL database, the following function calls will convert a string value into a + * numeric value in the same way that the standard implementations in this package do. This may be useful in + * writing SQL statements for use with + * {@code AbstractJDBCDataModel} subclasses which convert string + * column values to appropriate numeric values -- though this should be viewed as a temporary arrangement + * since it will impact performance: + * </p> + * + * <p> + * {@code cast(conv(substring(md5([column name]), 1, 16),16,10) as signed)} + * </p> + */ +public final class MySQLJDBCIDMigrator extends AbstractJDBCIDMigrator { + + public MySQLJDBCIDMigrator(DataSource dataSource) { + this(dataSource, DEFAULT_MAPPING_TABLE, + DEFAULT_LONG_ID_COLUMN, DEFAULT_STRING_ID_COLUMN); + } + + public MySQLJDBCIDMigrator(DataSource dataSource, + String mappingTable, + String longIDColumn, + String stringIDColumn) { + super(dataSource, + "SELECT " + stringIDColumn + " FROM " + mappingTable + " WHERE " + longIDColumn + "=?", + "INSERT IGNORE INTO " + mappingTable + " (" + longIDColumn + ',' + stringIDColumn + ") VALUES (?,?)"); + } + +} http://git-wip-us.apache.org/repos/asf/mahout/blob/5eda9e1f/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/PlusAnonymousConcurrentUserDataModel.java ---------------------------------------------------------------------- diff --git a/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/PlusAnonymousConcurrentUserDataModel.java b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/PlusAnonymousConcurrentUserDataModel.java new file mode 100644 index 0000000..c97a545 --- /dev/null +++ b/community/mahout-mr/src/main/java/org/apache/mahout/cf/taste/impl/model/PlusAnonymousConcurrentUserDataModel.java @@ -0,0 +1,352 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.mahout.cf.taste.impl.model; + +import com.google.common.base.Preconditions; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; + +import com.google.common.collect.Lists; +import org.apache.mahout.cf.taste.common.NoSuchItemException; +import org.apache.mahout.cf.taste.common.TasteException; +import org.apache.mahout.cf.taste.impl.common.FastIDSet; +import org.apache.mahout.cf.taste.impl.common.LongPrimitiveIterator; +import org.apache.mahout.cf.taste.model.DataModel; +import org.apache.mahout.cf.taste.model.Preference; +import org.apache.mahout.cf.taste.model.PreferenceArray; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * <p> + * This is a special thread-safe version of {@link PlusAnonymousUserDataModel} + * which allow multiple concurrent anonymous requests. + * </p> + * + * <p> + * To use it, you have to estimate the number of concurrent anonymous users of your application. + * The pool of users with the given size will be created. For each anonymous recommendations request, + * a user has to be taken from the pool and returned back immediately afterwards. + * </p> + * + * <p> + * If no more users are available in the pool, anonymous recommendations cannot be produced. + * </p> + * + * </p> + * + * Setup: + * <pre> + * int concurrentUsers = 100; + * DataModel realModel = .. + * PlusAnonymousConcurrentUserDataModel plusModel = + * new PlusAnonymousConcurrentUserDataModel(realModel, concurrentUsers); + * Recommender recommender = ...; + * </pre> + * + * Real-time recommendation: + * <pre> + * PlusAnonymousConcurrentUserDataModel plusModel = + * (PlusAnonymousConcurrentUserDataModel) recommender.getDataModel(); + * + * // Take the next available anonymous user from the pool + * Long anonymousUserID = plusModel.takeAvailableUser(); + * + * PreferenceArray tempPrefs = .. + * tempPrefs.setUserID(0, anonymousUserID); + * tempPrefs.setItemID(0, itemID); + * plusModel.setTempPrefs(tempPrefs, anonymousUserID); + * + * // Produce recommendations + * recommender.recommend(anonymousUserID, howMany); + * + * // It is very IMPORTANT to release user back to the pool + * plusModel.releaseUser(anonymousUserID); + * </pre> + * + * </p> + */ +public final class PlusAnonymousConcurrentUserDataModel extends PlusAnonymousUserDataModel { + + /** Preferences for all anonymous users */ + private final Map<Long,PreferenceArray> tempPrefs; + /** Item IDs set for all anonymous users */ + private final Map<Long,FastIDSet> prefItemIDs; + /** Pool of the users (FIFO) */ + private Queue<Long> usersPool; + + private static final Logger log = LoggerFactory.getLogger(PlusAnonymousUserDataModel.class); + + /** + * @param delegate Real model where anonymous users will be added to + * @param maxConcurrentUsers Maximum allowed number of concurrent anonymous users + */ + public PlusAnonymousConcurrentUserDataModel(DataModel delegate, int maxConcurrentUsers) { + super(delegate); + + tempPrefs = new ConcurrentHashMap<>(); + prefItemIDs = new ConcurrentHashMap<>(); + + initializeUsersPools(maxConcurrentUsers); + } + + /** + * Initialize the pool of concurrent anonymous users. + * + * @param usersPoolSize Maximum allowed number of concurrent anonymous user. Depends on the consumer system. + */ + private void initializeUsersPools(int usersPoolSize) { + usersPool = new ConcurrentLinkedQueue<>(); + for (int i = 0; i < usersPoolSize; i++) { + usersPool.add(TEMP_USER_ID + i); + } + } + + /** + * Take the next available concurrent anonymous users from the pool. + * + * @return User ID or null if no more users are available + */ + public Long takeAvailableUser() { + Long takenUserID = usersPool.poll(); + if (takenUserID != null) { + // Initialize the preferences array to indicate that the user is taken. + tempPrefs.put(takenUserID, new GenericUserPreferenceArray(0)); + return takenUserID; + } + return null; + } + + /** + * Release previously taken anonymous user and return it to the pool. + * + * @param userID ID of a previously taken anonymous user + * @return true if the user was previously taken, false otherwise + */ + public boolean releaseUser(Long userID) { + if (tempPrefs.containsKey(userID)) { + this.clearTempPrefs(userID); + // Return previously taken user to the pool + usersPool.offer(userID); + return true; + } + return false; + } + + /** + * Checks whether a given user is a valid previously acquired anonymous user. + */ + private boolean isAnonymousUser(long userID) { + return tempPrefs.containsKey(userID); + } + + /** + * Sets temporary preferences for a given anonymous user. + */ + public void setTempPrefs(PreferenceArray prefs, long anonymousUserID) { + Preconditions.checkArgument(prefs != null && prefs.length() > 0, "prefs is null or empty"); + + this.tempPrefs.put(anonymousUserID, prefs); + FastIDSet userPrefItemIDs = new FastIDSet(); + + for (int i = 0; i < prefs.length(); i++) { + userPrefItemIDs.add(prefs.getItemID(i)); + } + + this.prefItemIDs.put(anonymousUserID, userPrefItemIDs); + } + + /** + * Clears temporary preferences for a given anonymous user. + */ + public void clearTempPrefs(long anonymousUserID) { + this.tempPrefs.remove(anonymousUserID); + this.prefItemIDs.remove(anonymousUserID); + } + + @Override + public LongPrimitiveIterator getUserIDs() throws TasteException { + // Anonymous users have short lifetime and should not be included into the neighbohoods of the real users. + // Thus exclude them from the universe. + return getDelegate().getUserIDs(); + } + + @Override + public PreferenceArray getPreferencesFromUser(long userID) throws TasteException { + if (isAnonymousUser(userID)) { + return tempPrefs.get(userID); + } + return getDelegate().getPreferencesFromUser(userID); + } + + @Override + public FastIDSet getItemIDsFromUser(long userID) throws TasteException { + if (isAnonymousUser(userID)) { + return prefItemIDs.get(userID); + } + return getDelegate().getItemIDsFromUser(userID); + } + + @Override + public PreferenceArray getPreferencesForItem(long itemID) throws TasteException { + if (tempPrefs.isEmpty()) { + return getDelegate().getPreferencesForItem(itemID); + } + + PreferenceArray delegatePrefs = null; + + try { + delegatePrefs = getDelegate().getPreferencesForItem(itemID); + } catch (NoSuchItemException nsie) { + // OK. Probably an item that only the anonymous user has + if (log.isDebugEnabled()) { + log.debug("Item {} unknown", itemID); + } + } + + List<Preference> anonymousPreferences = Lists.newArrayList(); + + for (Map.Entry<Long, PreferenceArray> prefsMap : tempPrefs.entrySet()) { + PreferenceArray singleUserTempPrefs = prefsMap.getValue(); + for (int i = 0; i < singleUserTempPrefs.length(); i++) { + if (singleUserTempPrefs.getItemID(i) == itemID) { + anonymousPreferences.add(singleUserTempPrefs.get(i)); + } + } + } + + int delegateLength = delegatePrefs == null ? 0 : delegatePrefs.length(); + int anonymousPrefsLength = anonymousPreferences.size(); + int prefsCounter = 0; + + // Merge the delegate and anonymous preferences into a single array + PreferenceArray newPreferenceArray = new GenericItemPreferenceArray(delegateLength + anonymousPrefsLength); + + for (int i = 0; i < delegateLength; i++) { + newPreferenceArray.set(prefsCounter++, delegatePrefs.get(i)); + } + + for (Preference anonymousPreference : anonymousPreferences) { + newPreferenceArray.set(prefsCounter++, anonymousPreference); + } + + if (newPreferenceArray.length() == 0) { + // No, didn't find it among the anonymous user prefs + throw new NoSuchItemException(itemID); + } + + return newPreferenceArray; + } + + @Override + public Float getPreferenceValue(long userID, long itemID) throws TasteException { + if (isAnonymousUser(userID)) { + PreferenceArray singleUserTempPrefs = tempPrefs.get(userID); + for (int i = 0; i < singleUserTempPrefs.length(); i++) { + if (singleUserTempPrefs.getItemID(i) == itemID) { + return singleUserTempPrefs.getValue(i); + } + } + return null; + } + return getDelegate().getPreferenceValue(userID, itemID); + } + + @Override + public Long getPreferenceTime(long userID, long itemID) throws TasteException { + if (isAnonymousUser(userID)) { + // Timestamps are not saved for anonymous preferences + return null; + } + return getDelegate().getPreferenceTime(userID, itemID); + } + + @Override + public int getNumUsers() throws TasteException { + // Anonymous users have short lifetime and should not be included into the neighbohoods of the real users. + // Thus exclude them from the universe. + return getDelegate().getNumUsers(); + } + + @Override + public int getNumUsersWithPreferenceFor(long itemID) throws TasteException { + if (tempPrefs.isEmpty()) { + return getDelegate().getNumUsersWithPreferenceFor(itemID); + } + + int countAnonymousUsersWithPreferenceFor = 0; + + for (Map.Entry<Long, PreferenceArray> singleUserTempPrefs : tempPrefs.entrySet()) { + for (int i = 0; i < singleUserTempPrefs.getValue().length(); i++) { + if (singleUserTempPrefs.getValue().getItemID(i) == itemID) { + countAnonymousUsersWithPreferenceFor++; + break; + } + } + } + return getDelegate().getNumUsersWithPreferenceFor(itemID) + countAnonymousUsersWithPreferenceFor; + } + + @Override + public int getNumUsersWithPreferenceFor(long itemID1, long itemID2) throws TasteException { + if (tempPrefs.isEmpty()) { + return getDelegate().getNumUsersWithPreferenceFor(itemID1, itemID2); + } + + int countAnonymousUsersWithPreferenceFor = 0; + + for (Map.Entry<Long, PreferenceArray> singleUserTempPrefs : tempPrefs.entrySet()) { + boolean found1 = false; + boolean found2 = false; + for (int i = 0; i < singleUserTempPrefs.getValue().length() && !(found1 && found2); i++) { + long itemID = singleUserTempPrefs.getValue().getItemID(i); + if (itemID == itemID1) { + found1 = true; + } + if (itemID == itemID2) { + found2 = true; + } + } + + if (found1 && found2) { + countAnonymousUsersWithPreferenceFor++; + } + } + + return getDelegate().getNumUsersWithPreferenceFor(itemID1, itemID2) + countAnonymousUsersWithPreferenceFor; + } + + @Override + public void setPreference(long userID, long itemID, float value) throws TasteException { + if (isAnonymousUser(userID)) { + throw new UnsupportedOperationException(); + } + getDelegate().setPreference(userID, itemID, value); + } + + @Override + public void removePreference(long userID, long itemID) throws TasteException { + if (isAnonymousUser(userID)) { + throw new UnsupportedOperationException(); + } + getDelegate().removePreference(userID, itemID); + } +}
