/**
* 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.similarity.jdbc;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Collection;
import javax.sql.DataSource;
import org.apache.mahout.cf.taste.common.Refreshable;
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.jdbc.AbstractJDBCComponent;
import org.apache.mahout.cf.taste.impl.model.jdbc.ConnectionPoolDataSource;
import org.apache.mahout.cf.taste.similarity.ItemSimilarity;
import org.apache.mahout.common.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* An {@link ItemSimilarity} which draws pre-computed item-item similarities from a database table via JDBC.
*/
public abstract class AbstractJDBCItemSimilarity extends AbstractJDBCComponent implements ItemSimilarity {
private static final Logger log = LoggerFactory.getLogger(AbstractJDBCItemSimilarity.class);
static final String DEFAULT_SIMILARITY_TABLE = "taste_item_similarity";
static final String DEFAULT_ITEM_A_ID_COLUMN = "item_id_a";
static final String DEFAULT_ITEM_B_ID_COLUMN = "item_id_b";
static final String DEFAULT_SIMILARITY_COLUMN = "similarity";
private final DataSource dataSource;
private final String similarityTable;
private final String itemAIDColumn;
private final String itemBIDColumn;
private final String similarityColumn;
private final String getItemItemSimilaritySQL;
private final String getAllSimilarItemIDsSQL;
protected AbstractJDBCItemSimilarity(DataSource dataSource,
String getItemItemSimilaritySQL,
String getAllSimilarItemIDsSQL) {
this(dataSource,
DEFAULT_SIMILARITY_TABLE,
DEFAULT_ITEM_A_ID_COLUMN,
DEFAULT_ITEM_B_ID_COLUMN,
DEFAULT_SIMILARITY_COLUMN,
getItemItemSimilaritySQL,
getAllSimilarItemIDsSQL);
}
protected AbstractJDBCItemSimilarity(DataSource dataSource,
String similarityTable,
String itemAIDColumn,
String itemBIDColumn,
String similarityColumn,
String getItemItemSimilaritySQL,
String getAllSimilarItemIDsSQL) {
AbstractJDBCComponent.checkNotNullAndLog("similarityTable", similarityTable);
AbstractJDBCComponent.checkNotNullAndLog("itemAIDColumn", itemAIDColumn);
AbstractJDBCComponent.checkNotNullAndLog("itemBIDColumn", itemBIDColumn);
AbstractJDBCComponent.checkNotNullAndLog("similarityColumn", similarityColumn);
AbstractJDBCComponent.checkNotNullAndLog("getItemItemSimilaritySQL", getItemItemSimilaritySQL);
AbstractJDBCComponent.checkNotNullAndLog("getAllSimilarItemIDsSQL", getAllSimilarItemIDsSQL);
if (!(dataSource instanceof ConnectionPoolDataSource)) {
log.warn("You are not using ConnectionPoolDataSource. Make sure your DataSource pools connections "
+ "to the database itself, or database performance will be severely reduced.");
}
this.dataSource = dataSource;
this.similarityTable = similarityTable;
this.itemAIDColumn = itemAIDColumn;
this.itemBIDColumn = itemBIDColumn;
this.similarityColumn = similarityColumn;
this.getItemItemSimilaritySQL = getItemItemSimilaritySQL;
this.getAllSimilarItemIDsSQL = getAllSimilarItemIDsSQL;
}
protected String getSimilarityTable() {
return similarityTable;
}
protected String getItemAIDColumn() {
return itemAIDColumn;
}
protected String getItemBIDColumn() {
return itemBIDColumn;
}
protected String getSimilarityColumn() {
return similarityColumn;
}
@Override
public double itemSimilarity(long itemID1, long itemID2) throws TasteException {
if (itemID1 == itemID2) {
return 1.0;
}
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(getItemItemSimilaritySQL, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchDirection(ResultSet.FETCH_FORWARD);
stmt.setFetchSize(getFetchSize());
return doItemSimilarity(stmt, itemID1, itemID2);
} catch (SQLException sqle) {
log.warn("Exception while retrieving similarity", sqle);
throw new TasteException(sqle);
} finally {
IOUtils.quietClose(null, stmt, conn);
}
}
@Override
public double[] itemSimilarities(long itemID1, long[] itemID2s) throws TasteException {
double[] result = new double[itemID2s.length];
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(getItemItemSimilaritySQL, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
stmt.setFetchDirection(ResultSet.FETCH_FORWARD);
stmt.setFetchSize(getFetchSize());
for (int i = 0; i < itemID2s.length; i++) {
result[i] = doItemSimilarity(stmt, itemID1, itemID2s[i]);
}
} catch (SQLException sqle) {
log.warn("Exception while retrieving item similarities", sqle);
throw new TasteException(sqle);
} finally {
IOUtils.quietClose(null, stmt, conn);
}
return result;
}
@Override
public long[] allSimilarItemIDs(long itemID) throws TasteException {
FastIDSet allSimilarItemIDs = new FastIDSet();
Connection conn = null;
PreparedStatement stmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
stmt = conn.prepareStatement(getAllSimilarItemIDsSQL, ResultSet.TYPE_FORWARD_ONLY,
ResultSet.CONCUR_READ_ONLY);
stmt.setFetchDirection(ResultSet.FETCH_FORWARD);
stmt.setFetchSize(getFetchSize());
stmt.setLong(1, itemID);
stmt.setLong(2, itemID);
rs = stmt.executeQuery();
while (rs.next()) {
allSimilarItemIDs.add(rs.getLong(1));
allSimilarItemIDs.add(rs.getLong(2));
}
} catch (SQLException sqle) {
log.warn("Exception while retrieving all similar itemIDs", sqle);
throw new TasteException(sqle);
} finally {
IOUtils.quietClose(rs, stmt, conn);
}
allSimilarItemIDs.remove(itemID);
return allSimilarItemIDs.toArray();
}
@Override
public void refresh(Collection<Refreshable> alreadyRefreshed) {
// do nothing
}
private double doItemSimilarity(PreparedStatement stmt, long itemID1, long itemID2) throws SQLException {
// Order as smaller - larger
if (itemID1 > itemID2) {
long temp = itemID1;
itemID1 = itemID2;
itemID2 = temp;
}
stmt.setLong(1, itemID1);
stmt.setLong(2, itemID2);
log.debug("Executing SQL query: {}", getItemItemSimilaritySQL);
ResultSet rs = null;
try {
rs = stmt.executeQuery();
// If not found, perhaps the items exist but have no presence in the table,
// so NaN is appropriate
return rs.next() ? rs.getDouble(1) : Double.NaN;
} finally {
IOUtils.quietClose(rs);
}
}
}