| 1 |
<?php |
<?php |
| 2 |
// $Id: cre.module,v 1.9.2.2.2.12 2007/07/23 13:51:27 hickory Exp $ |
// $Id: $ |
| 3 |
/* |
/* |
| 4 |
A generalized recommendation engine. Depends on votingapi.module. see README.txt |
A generalized recommendation engine. Depends on votingapi.module. see README.txt |
| 5 |
*/ |
*/ |
| 21 |
*/ |
*/ |
| 22 |
function cre_menu($may_cache) { |
function cre_menu($may_cache) { |
| 23 |
$items = array(); |
$items = array(); |
| 24 |
if ($may_cache) { |
if ($may_cache) { |
| 25 |
$items[] = array('path' => 'admin/settings/cre', |
$items[] = array('path' => 'admin/settings/cre', |
|
'title' => t('CRE'), |
|
|
'description' => t('Various settings to control how and what content to recommend'), |
|
|
'callback' => 'cre_overview', |
|
|
'type' => MENU_NORMAL_ITEM |
|
|
); |
|
|
|
|
|
$items[] = array('path' => 'admin/settings/cre/settings', |
|
| 26 |
'title' => t('Content Recommendation Engine'), |
'title' => t('Content Recommendation Engine'), |
| 27 |
'description' => t('Site-wide settings for the Content Recommendation Engine'), |
'description' => t('Site-wide settings for the Content Recommendation Engine'), |
| 28 |
'callback' => 'drupal_get_form', |
'callback' => 'drupal_get_form', |
| 34 |
return $items; |
return $items; |
| 35 |
} |
} |
| 36 |
|
|
|
/** |
|
|
* Menu callback; displays the cre admin overview. |
|
|
*/ |
|
|
function cre_overview() { // FIXME: necessary? |
|
|
$menu = menu_get_item(NULL, 'admin/settings/cre'); |
|
|
$content = system_admin_menu_block($menu); |
|
|
$output = theme('admin_block_content', $content); |
|
|
return $output; |
|
|
} |
|
|
|
|
| 37 |
function cre_settings() { |
function cre_settings() { |
| 38 |
$form['cre_max_install_votes'] = array( |
$form['cre_max_install_votes'] = array( |
| 39 |
'#type' => 'textfield', |
'#type' => 'textfield', |
| 42 |
'#default_value' => variable_get('cre_max_install_votes', 0), |
'#default_value' => variable_get('cre_max_install_votes', 0), |
| 43 |
); |
); |
| 44 |
|
|
| 45 |
|
$start = variable_get('cre_starting_position', 0); |
| 46 |
|
$max = db_result(db_query("SELECT MAX(vote_id) FROM votingapi_vote")); |
| 47 |
|
$form['cre_index_status'] = array( |
| 48 |
|
'#type' => 'markup', |
| 49 |
|
'#value' => '<strong><em>' . $start / $max . '</em></strong> of the site indexed', |
| 50 |
|
); |
| 51 |
|
|
| 52 |
$form['cre_query_type'] = array( |
$form['cre_query_type'] = array( |
| 53 |
'#type' => 'radios', |
'#type' => 'radios', |
| 54 |
'#title' => t('Select a query type'), |
'#title' => t('Select a query type'), |
| 81 |
function cre_modify_avg_difference($uid, $nid, $content_type) { |
function cre_modify_avg_difference($uid, $nid, $content_type) { |
| 82 |
if ($result = db_query("SELECT DISTINCT r.content_id, r2.value - r.value as rating_difference, r.content_type as content_type1, r2.content_type as content_type2 FROM {votingapi_vote} r, {votingapi_vote} r2 WHERE r.uid = %d AND r2.content_id = %d AND r2.uid = r.uid AND r.content_type = '%s'", $uid, $nid, $content_type)) { |
if ($result = db_query("SELECT DISTINCT r.content_id, r2.value - r.value as rating_difference, r.content_type as content_type1, r2.content_type as content_type2 FROM {votingapi_vote} r, {votingapi_vote} r2 WHERE r.uid = %d AND r2.content_id = %d AND r2.uid = r.uid AND r.content_type = '%s'", $uid, $nid, $content_type)) { |
| 83 |
while ($row = db_fetch_object($result)){ |
while ($row = db_fetch_object($result)){ |
| 84 |
|
|
| 85 |
// check to see if the pair of content ($itemID and $other_node) are already in the cre_similarity_matrix table |
db_query("UPDATE {cre_similarity_matrix} SET count = count + 1, sum = sum + %d WHERE content_id1 = %d AND content_id2 = %d AND content_type1 = '%s'", $row->rating_difference, $nid, $row->content_id, $content_type); |
| 86 |
if (db_result(db_query("SELECT COUNT(*) FROM {cre_similarity_matrix} WHERE content_id1 = %d AND content_id2 = %d AND content_type1 = '%s'", $nid, $row->content_id, $content_type))){ |
// update the second row only if the two nodes are different |
| 87 |
//update the two rows |
if ($nid != $row->content_id) { |
| 88 |
db_query("UPDATE {cre_similarity_matrix} SET count = count + 1, sum = sum + %d WHERE content_id1 = %d AND content_id2 = %d AND content_type1 = '%s'", $row->rating_difference, $nid, $row->content_id, $content_type); |
db_query("UPDATE {cre_similarity_matrix} SET count=count+1, sum=sum-%d WHERE content_id1 = %d AND content_id2 = %d AND content_type1 = '%s'", $row->rating_difference, $row->content_id, $nid, $content_type); |
|
|
|
|
// update the second row only if the two nodes are different |
|
|
if ($nid != $row->content_id) { |
|
|
db_query("UPDATE {cre_similarity_matrix} SET count=count+1, sum=sum-%d WHERE content_id1 = %d AND content_id2 = %d AND content_type1 = '%s'", $row->rating_difference, $row->content_id, $nid, $content_type); |
|
|
} |
|
| 89 |
} |
} |
| 90 |
else { |
if (!db_affected_rows()) { |
| 91 |
// if this is a new 'pairing' create two rows |
// if this is a new 'pairing' create two rows |
| 92 |
db_query("INSERT INTO {cre_similarity_matrix} (content_id1, content_id2, content_type1, content_type2, count, sum) VALUES (%d, %d, '%s', '%s', 1, %d)", $nid, $row->content_id, $row->content_type1, $row->content_type2, $row->rating_difference); |
db_query("INSERT INTO {cre_similarity_matrix} (content_id1, content_id2, content_type1, content_type2, count, sum) VALUES (%d, %d, '%s', '%s', 1, %d)", $nid, $row->content_id, $row->content_type1, $row->content_type2, $row->rating_difference); |
| 93 |
|
|
| 101 |
} |
} |
| 102 |
} |
} |
| 103 |
|
|
|
/* |
|
|
* Returns an array of the top n content |
|
|
* |
|
|
* @param $uid |
|
|
* user id for the personalized recommendation |
|
|
* |
|
|
*@param $calling_module |
|
|
*specifies the name of the calling module. This string is then used to invoke hook_cre_query() in that module |
|
|
*and only that module. |
|
|
* |
|
|
* @param $n default 10 |
|
|
* specifies the number of contents to return |
|
|
* |
|
|
* @param $target_type |
|
|
* the type of content that the calling function would like |
|
|
* |
|
|
* @param $tag default 'vote' |
|
|
* same as in votingapi. See votingapi |
|
|
* |
|
|
* @param $reference_type default NULL |
|
|
* When set only those votes whos content_type is equal to this |
|
|
* will be considered for the recommendations. |
|
|
* |
|
|
* @return |
|
|
* returns an array of objs with content_id and score |
|
|
*/ |
|
|
function cre_top($uid, $callback, $n = 10, $target_type = 'node', $tag = 'vote', $reference_type = NULL, $callback_args = array()){ |
|
|
|
|
|
// create query obj |
|
|
$query = new _cre_query($uid, $target_type, $tag, $reference_type); |
|
|
|
|
|
// invoke callback function |
|
|
$callback($query, $uid, $callback_args); |
|
|
//dprint_r($query); |
|
|
|
|
|
// execute query obj |
|
|
if (!($query_string = $query->execute()) || !($result = db_query($query_string))) return NULL; |
|
|
//dprint_r($query_string); |
|
|
|
|
|
// check to see if recommending based on votingapi point system - use percent system for accurate or fast system, otherwise points |
|
|
$value_type = (variable_get('cre_query_type', 'accurate') == 'accurate' || 'fast') ? 'percent' : 'points'; |
|
|
|
|
|
$count = 0; |
|
|
$items = array(); |
|
|
while ($item = db_fetch_object($result)) { |
|
|
// use this content if it hasn't been rated by the uid |
|
|
if (!$user_check_result = votingapi_get_vote($target_type, $item->content_id, $value_type, $tag, $uid)) { |
|
|
$items[] = $item; |
|
|
$count++; |
|
|
} |
|
|
if ($count == $n) break; |
|
|
} |
|
|
|
|
|
// do something special when user has rated all nodes: return top rated nodes? |
|
|
return empty($items) ? '' : $items; |
|
|
} |
|
|
|
|
|
/* |
|
|
* Returns an array of content objs |
|
|
* |
|
|
* @param $n |
|
|
* The number of objs to return |
|
|
* |
|
|
* @param $nid |
|
|
* content_id that the return objects will be similar too |
|
|
* |
|
|
* @param $target_type |
|
|
* Type of content to return from this function |
|
|
* |
|
|
* @param $tag |
|
|
* votingapi tag. See Votingapi.module |
|
|
* |
|
|
* @param $reference_type |
|
|
* specifies the type of content that will be only be used to determine the revelenance of |
|
|
* a certain piece of content |
|
|
* |
|
|
* @return |
|
|
* $return_value[] is an array of objs that have two fields, content_id and average. |
|
|
* |
|
|
*/ |
|
| 104 |
|
|
|
function cre_similar($n, $nid, $target_type = 'node', $tag = 'vote', $reference_type = NULL) { |
|
|
// TODO: allow for reference_type to be an array |
|
|
$node = node_load($nid); |
|
|
|
|
|
if ($reference_type) { |
|
|
$sql_result = db_query("SELECT d.content_id2 as 'content_id', (d.sum / d.count) AS 'average', n.title |
|
|
FROM {cre_similarity_matrix} d JOIN {node} n ON d.content_id2 = n.nid |
|
|
WHERE d.content_id1 = %d AND d.content_id2 <> %d AND content_type1 = '%s' AND content_type2 = '%s' |
|
|
ORDER BY (sum/count) DESC LIMIT %d", $nid, $nid, $reference_type, $target_type, $n); |
|
|
} |
|
|
else { |
|
|
$sql_result = db_query("SELECT d.content_id2 as 'content_id', (d.sum / d.count) AS 'average', n.title |
|
|
FROM {cre_similarity_matrix} d JOIN {node} n ON d.content_id2 = n.nid |
|
|
WHERE d.content_id1 = %d AND d.content_id2 <> %d AND content_type2 = '%s' AND n.type = '%s' |
|
|
ORDER BY (sum/count) DESC LIMIT %d", $nid, $nid, $target_type, $node->type, $n); |
|
|
} |
|
|
$return_value = array(); |
|
|
while ($recommend_obj = db_fetch_object($sql_result)) { |
|
|
$return_value[] = $recommend_obj; |
|
|
} |
|
|
return $return_value; |
|
|
|
|
|
} |
|
| 105 |
|
|
| 106 |
/* |
/* |
| 107 |
* implementation of votingapi's hook_insert() |
* implementation of votingapi's hook_insert() |
| 109 |
|
|
| 110 |
function cre_votingapi_insert(&$vote){ |
function cre_votingapi_insert(&$vote){ |
| 111 |
if ($vote->value_type != 'percent' && $vote->value_type != 'points') return; |
if ($vote->value_type != 'percent' && $vote->value_type != 'points') return; |
|
|
|
|
cre_modify_avg_difference($vote->uid, $vote->content_id, $vote->content_type); |
|
| 112 |
|
|
| 113 |
|
// just update the average table, the similarity matrix will be updated on cron |
| 114 |
// update cre_average_vote table |
// update cre_average_vote table |
| 115 |
if (db_result(db_query("SELECT COUNT(*) from {cre_average_vote} where uid = %d", $vote->uid))) { |
db_query("UPDATE {cre_average_vote} SET count = count + 1, sum = sum + %d WHERE uid = %d", $vote->value, $vote->uid); |
| 116 |
db_query("UPDATE {cre_average_vote} SET count = count + 1, sum = sum + %d WHERE uid = %d", $vote->value, $vote->uid); |
if (!db_affected_rows()) { |
|
} |
|
|
else { |
|
| 117 |
db_query("INSERT INTO {cre_average_vote} (uid, sum,count) VALUES (%d, %d, %d)", $vote->uid, $vote->value, 1); |
db_query("INSERT INTO {cre_average_vote} (uid, sum,count) VALUES (%d, %d, %d)", $vote->uid, $vote->value, 1); |
| 118 |
} |
} |
| 119 |
return; |
return; |
| 120 |
} |
} |
| 121 |
|
|
| 122 |
/* |
/* |
|
* implementation of votingapi's hook_update |
|
|
*/ |
|
|
function cre_votingapi_update(&$vote, $new_value){ |
|
|
// When a vote is updated, adjust sum - not count - for all records in cre_similarity_matrix where content_id1 or content_id2 equals vote->content_id. |
|
|
// The adjustment to sum is the rating difference. |
|
|
|
|
|
$rating_difference = $new_value - $vote->value; |
|
|
|
|
|
db_query("UPDATE {cre_similarity_matrix} SET sum = sum + %d WHERE (content_id1 = %d AND content_type1 = '%s') OR (content_id2 = %d AND content_type2 = '%s')", $rating_difference, $vote->content_id, $vote->content_type, $vote->content_id, $vote->content_type); |
|
|
|
|
|
db_query("UPDATE {cre_average_vote} SET sum = sum + %d WHERE uid = %d", $rating_difference, $vote->uid); |
|
|
} |
|
|
|
|
|
/* |
|
| 123 |
* implementation of votingapi's hook_delete |
* implementation of votingapi's hook_delete |
| 124 |
*/ |
*/ |
| 125 |
function cre_votingapi_delete(&$vote){ |
function cre_votingapi_delete(&$vote){ |
| 144 |
* private function to install the module over existing voting data |
* private function to install the module over existing voting data |
| 145 |
*/ |
*/ |
| 146 |
function _cre_load_all_diff_avg(){ |
function _cre_load_all_diff_avg(){ |
| 147 |
$starting_pos = variable_get('cre_starting_position', -1); |
global $pos; |
| 148 |
|
register_shutdown_function('cre_shutdown'); |
| 149 |
|
$starting_pos = variable_get('cre_starting_position', 0); |
| 150 |
$vote_limit = variable_get('cre_max_install_votes', FALSE); |
$vote_limit = variable_get('cre_max_install_votes', FALSE); |
| 151 |
|
|
| 152 |
// for every vote within range...^ |
// for every vote within range...^ |
| 153 |
$db_result = db_query("SELECT vote_id, uid, content_id, content_type FROM {votingapi_vote} WHERE vote_id < %d ORDER BY vote_id DESC LIMIT %d", $starting_pos, $vote_limit); |
$db_result = db_query("SELECT vote_id, uid, content_id, content_type FROM {votingapi_vote} WHERE vote_id > %d ORDER BY vote_id ASC LIMIT %d", $starting_pos, $vote_limit); |
| 154 |
while ($vote_obj = db_fetch_object($db_result)) { |
while ($vote_obj = db_fetch_object($db_result)) { |
| 155 |
// call function cre_modify_avg_difference($uid, $content_id) |
// call function cre_modify_avg_difference($uid, $content_id) |
| 156 |
cre_modify_avg_difference($vote_obj->uid, $vote_obj->content_id, $vote_obj->content_type); |
cre_modify_avg_difference($vote_obj->uid, $vote_obj->content_id, $vote_obj->content_type); |
| 157 |
$vid = $vote_obj->vote_id; |
$pos = $vote_obj->vote_id; |
| 158 |
} |
} |
|
// save the starting position for next run |
|
|
variable_set('cre_starting_position', $vid); |
|
| 159 |
} |
} |
| 160 |
|
|
| 161 |
function cre_cron() { |
function cre_shutdown() { |
| 162 |
if (variable_get('cre_max_install_votes', 0)){ |
global $pos; |
| 163 |
_cre_load_all_diff_avg(); |
// save the starting position for next run |
| 164 |
} |
variable_set('cre_starting_position', $pos); |
| 165 |
} |
} |
| 166 |
|
|
| 167 |
/* |
function cre_cron() { |
| 168 |
* This function will be responsible for installing the users' average vote |
_cre_load_all_diff_avg(); |
|
*/ |
|
|
|
|
|
function _cre_user_average() { |
|
|
$result = db_query("SELECT DISTINCT uid from {votingapi_vote}"); |
|
|
while ($user = db_fetch_object($result)) { |
|
|
// get all 'percent' vote values made by this user |
|
|
$total = db_result(db_query("SELECT SUM(value) from {votingapi_vote} WHERE uid = %d AND value_type = 'percent'", $user->uid)); |
|
|
$num_rows = db_num_rows(db_query("SELECT vote_id from {votingapi_vote} where uid = %d", $user->uid)); |
|
|
|
|
|
db_query("INSERT INTO {cre_average_vote} (uid, sum, count) VALUES (%d, %d, %d)", $user->uid, $total, $num_rows); |
|
|
} |
|
| 169 |
} |
} |
| 170 |
|
|
| 171 |
function cre_remove($content_id, $content_type) { |
function cre_remove($content_id, $content_type) { |
| 172 |
db_query("DELETE FROM {cre_similarity_matrix} WHERE content_id1 = %d AND content_type1 = '%s'", $content_id, $content_type); |
db_query("DELETE FROM {cre_similarity_matrix} WHERE content_id1 = %d AND content_type1 = '%s'", $content_id, $content_type); |
| 173 |
db_query("DELETE FROM {cre_similarity_matrix} WHERE content_id2 = %d AND content_type2 = '%s'", $content_id, $content_type); |
db_query("DELETE FROM {cre_similarity_matrix} WHERE content_id2 = %d AND content_type2 = '%s'", $content_id, $content_type); |
|
} |
|
|
|
|
|
function theme_cre_title_list($items = array()){ |
|
|
$output = array(); |
|
|
foreach ($items as $item){ |
|
|
$output[] = l($item->title, "node/$item->content_id", array('class' => 'node-' . $item->type)); |
|
|
} |
|
|
return theme('item_list', $output); |
|
|
} |
|
|
|
|
|
function theme_cre_node_view($nid){ |
|
|
return node_view(node_load(array('nid' => $nid)), FALSE); |
|
| 174 |
} |
} |