You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mahout.apache.org by ra...@apache.org on 2018/06/04 14:29:47 UTC
[45/53] [abbrv] [partial] mahout git commit: end of day 6-2-2018
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);
+ }
+}