similar.module in Similar Entries 7
Same filename and directory in other branches
Module that shows a block listing similar entries. NOTE: Uses MySQL's FULLTEXT indexing for MyISAM tables.
Caching feature sponsored by http://xomba.com/
@author David Kent Norman http://deekayen.net/ @author Arnab Nandi http://arnab.org/ @author Jordan Halterman jordan.halterman@gmail.com
File
similar.moduleView source
<?php
/**
* @file
* Module that shows a block listing similar entries.
* NOTE: Uses MySQL's FULLTEXT indexing for MyISAM tables.
*
* Caching feature sponsored by http://xomba.com/
*
* @author David Kent Norman http://deekayen.net/
* @author Arnab Nandi http://arnab.org/
* @author Jordan Halterman jordan.halterman@gmail.com
*/
/**
* Implements hook_help().
*/
function similar_help($path, $arg) {
switch ($path) {
case 'admin/help#similar':
return '<p>' . t('Lists the most similar nodes to the current node.') . '</p>';
}
}
/**
* Implements hook_theme().
*/
function similar_theme() {
return array(
'similar_content' => array(
'variables' => array(
'node' => NULL,
),
),
);
}
/**
* Implements hook_cron().
*
* Checks if an index rebuild is needed as determined in Drupal
* variables. If the similar_index variable is not empty it will
* have an array of field names which were added on the last
* cron run. If field settings have changed since that last cron
* run then the old index will be deleted and a new one added that
* includes all the relevant fields in the table. This is because
* ideally we want a single index for multiple fields in a table.
*/
function similar_cron() {
if (module_exists('field')) {
$indices = similar_get_indices();
// Get all text columns defined for fields in the database.
$data = array();
foreach (field_info_fields() as $field => $info) {
if ($info['type'] == 'text') {
$table = key($info['storage']['details']['sql'][FIELD_LOAD_CURRENT]);
$data[$table][] = $info['storage']['details']['sql'][FIELD_LOAD_CURRENT][$table]['value'];
}
}
// For each table check if the index needs to be reset.
foreach ($data as $table => $fields) {
_similar_add_index($table, $data[$table]);
}
}
}
/**
* Adds FULLTEXT indexes to field value database columns.
*
* We want to make sure that we index as many columns as are available
* with a single index to improve results. So we remove the old index
* from the table before then reindexing the new fields. Indexed fields
* are then stored in a Drupal variable which is an array of sub-arrays,
* with the array key the table name and the sub-array being the fields
* that are indexed. Before calling this function the $fields argument
* should be compared against the existing index as stored in the
* similar_indices variable.
*
* @param $table
* A string representing the table to index.
* @param $fields
* An array of column names to add to the index. Even though most,
* if not all of our tables will only have one indexed column, we
* still may need to add multiple fields some time.
*/
function _similar_add_index($table, $fields) {
$index = similar_get_indices();
if (db_table_exists($table)) {
// Drop the existing index on the table if it exists.
if (isset($index[$table]) && !empty($index[$table]) && db_index_exists($table, 'similar')) {
db_drop_index($table, 'similar');
unset($index[$table]);
}
if (!empty($fields)) {
$add_fields = implode(', ', $fields);
db_query("ALTER TABLE {$table} ENGINE = MYISAM");
db_query("ALTER TABLE {$table} ADD FULLTEXT `similar` ({$add_fields})");
$index[$table] = $fields;
}
}
elseif (isset($index[$table])) {
unset($index[$table]);
}
variable_set('similar_indices', $index);
}
/**
* Returns data about what fields are currently indexed.
*
* Indexed tables are fields are stored in a Drupal variable. Something
* to do is look into ways to reset the variable upon reasonable events
* like a field module install/uninstall or a cache clearance.
*
* @param $table
* An optional string identifying a specific table's indices to return.
* @return
* An array of sub-arrays where the key is a table name and the value
* is an array of fields which are currently indexed in the table.
*/
function similar_get_indices($table = NULL) {
$indices = variable_get('similar_indices', array());
if (!empty($table)) {
return isset($indices[$table]) ? $indices[$table] : FALSE;
}
else {
return $indices;
}
}
/**
* Implements hook_block_info().
*/
function similar_block_info() {
return array(
'similar' => array(
'info' => t('Similar Entries'),
'cache' => DRUPAL_CACHE_PER_PAGE,
),
);
}
/**
* Implements hook_block_configure().
*/
function similar_block_configure($delta = '') {
$form = array();
$form['list_settings'] = array(
'#type' => 'fieldset',
'#title' => t('List settings'),
'#collapsible' => TRUE,
);
$form['list_settings']['similar_summary_enabled'] = array(
'#type' => 'radios',
'#title' => t('Include summary text'),
'#default_value' => variable_get('similar_summary_enabled', 0),
'#options' => array(
t('No'),
t('Yes'),
),
);
$form['list_settings']['similar_rel_nofollow'] = array(
'#type' => 'radios',
'#title' => t('Block search engines'),
'#description' => t('Adds rel="nofollow" to the HTML source of similar links so search engines won\'t count similar links in their ranking calculations.'),
'#default_value' => variable_get('similar_rel_nofollow', 0),
'#options' => array(
t('No'),
t('Yes'),
),
);
for ($i = 1, $options = array(); $i < 101; $options[$i] = $i, $i += 1) {
}
$form['list_settings']['similar_num_display'] = array(
'#type' => 'select',
'#title' => t('Number of similar entries to find'),
'#default_value' => variable_get('similar_num_display', 5),
'#options' => $options,
);
$types = _similar_published_node_types();
$form['list_settings']['similar_node_types'] = array(
'#type' => 'checkboxes',
'#title' => t('Content types to display'),
'#default_value' => variable_get('similar_node_types', $types),
'#options' => $types,
'#multiple' => TRUE,
);
if (module_exists('field')) {
$form['list_settings']['similar_include_fields'] = array(
'#type' => 'checkbox',
'#title' => t('Include content fields in matching'),
'#default_value' => variable_get('similar_include_fields', 0),
'#description' => t('Include extra fields defined with Field module in database queries.'),
);
}
if (module_exists('taxonomy')) {
$names = _similar_taxonomy_names();
$form['similar_taxonomy'] = array(
'#type' => 'fieldset',
'#title' => t('Taxonomy category filter'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['similar_taxonomy']['similar_taxonomy_filter'] = array(
'#type' => 'radios',
'#title' => t('Filter by taxonomy categories'),
'#default_value' => variable_get('similar_taxonomy_filter', 0),
'#options' => array(
t('No category filtering'),
t('Only show the similar nodes in the same category as the original node'),
t('Use global category filtering'),
),
'#description' => t('By selecting global filtering, only nodes assigned to the following selected categories will display as similar nodes, regardless of the categories the original node is or is not assigned to.'),
);
$form['similar_taxonomy']['similar_taxonomy_select'] = array(
'#type' => 'fieldset',
'#title' => t('Taxonomy categories to display'),
'#collapsible' => TRUE,
'#collapsed' => TRUE,
);
$form['similar_taxonomy']['similar_taxonomy_select']['similar_taxonomy_tids'] = array(
'#type' => 'select',
'#default_value' => variable_get('similar_taxonomy_tids', array_keys($names)),
'#description' => t('Hold the CTRL key to (de)select multiple options.'),
'#options' => $names,
'#multiple' => TRUE,
);
}
return $form;
}
/**
* Implements hook_block_save().
*/
function similar_block_save($delta = '', $edit = array()) {
variable_set('similar_summary_enabled', $edit['similar_summary_enabled']);
variable_set('similar_rel_nofollow', $edit['similar_rel_nofollow']);
variable_set('similar_num_display', $edit['similar_num_display']);
variable_set('similar_node_types', $edit['similar_node_types']);
variable_set('similar_include_fields', $edit['similar_include_fields']);
if (module_exists('taxonomy')) {
variable_set('similar_taxonomy_filter', $edit['similar_taxonomy_filter']);
variable_set('similar_taxonomy_tids', $edit['similar_taxonomy_tids']);
}
}
/**
* Implements hook_block_view().
*/
function similar_block_view($delta = '') {
if (arg(0) == 'node' && is_numeric(arg(1)) && arg(2) != 'edit') {
$node = node_load(arg(1));
}
else {
return;
}
$similar_node_types = variable_get('similar_node_types', _similar_published_node_types());
if ($node->nid > 0 && !empty($similar_node_types[$node->type])) {
unset($similar_node_types);
// The subject is displayed at the top of the block. Note that it should
// be passed through t() for translation.
$block['subject'] = t('Similar entries');
$block['content'] = theme('similar_content', array(
'node' => $node,
));
}
return empty($block['content']) ? '' : $block;
}
/**
* Queries for published node types.
* @link http://drupal.org/node/33444 @endlink
*
* @return
* An array of node types that are available.
*/
function _similar_published_node_types() {
$types = array();
$result = db_select('node', 'n')
->fields('n', array(
'type',
))
->distinct()
->condition('n.status', 0, '<>')
->orderBy('n.type', 'ASC')
->execute()
->fetchAll();
foreach ($result as $type) {
$types[$type->type] = $type->type;
}
return $types;
}
/**
* Queries for taxonomy names.
* @link http://drupal.org/node/51041 @endlink
*
*@return
* An array of taxonomy term names to be used.
*/
function _similar_taxonomy_names() {
$names = array();
$query = db_select('taxonomy_term_data', 'd');
$query
->addField('d', 'tid');
$query
->addField('d', 'name', 'data_name');
$query
->innerJoin('taxonomy_vocabulary', 'v', 'd.vid = v.vid');
$query
->addField('v', 'name', 'vocab_name');
$query
->addField('v', 'vid');
$query
->fields('v', array(
'vid',
'name',
));
$query
->orderBy('v.name', 'ASC');
$query
->orderBy('d.name', 'ASC');
$result = $query
->execute()
->fetchAll();
foreach ($result as $data) {
$names[$data->tid] = $data->vocab_name . ': ' . $data->data_name;
}
return $names;
}
/**
* Queries for taxonomies to which a specific node belongs.
* @link http://drupal.org/node/51041 @endlink
*
* @param $nid
* A node ID.
* @return
* An array of terms related to the given node.
*/
function _similar_taxonomy_membership($nid) {
$tids = array();
$result = db_select('taxonomy_index', 'i')
->fields('i', array(
'tid',
))
->condition('i.nid', $nid, '=')
->execute()
->fetchAll();
foreach ($result as $data) {
$tids[$data->tid] = $data->tid;
}
return $tids;
}
/**
* Strips characters from node type strings.
*/
function _similar_content_type_escape(&$item) {
$item = str_replace(array(
"\0",
"\n",
"\r",
"\\",
"'",
"\"",
"\32",
), '', $item);
}
/**
* Prevents SQL injection be forcing an integer value.
*/
function _similar_force_int(&$item) {
$item = (int) $item;
}
/**
* Queries the database for similar entries and puts them in a HTML list.
*
* @param $node
* The current node being viewed.
* @return
* A themed item list of related links.
*/
function theme_similar_content($variables) {
$node = $variables['node'];
$boolean = variable_get('similar_boolean_mode', 1) == 1;
$summary = variable_get('similar_summary_enabled', 0);
$items = array();
$text = $node->title;
if (isset($node->body) && isset($node->body[$node->language])) {
$text .= " {$node->body[$node->language][0]['value']}";
}
// Create a comma-separated list of available node types.
$types = _similar_published_node_types();
$types = variable_get('similar_node_types', $types);
array_walk($types, '_similar_content_type_escape');
// Build the database query.
$query = db_select('node_revision', 'r');
$query
->addField('r', 'nid');
$query
->join('node', 'n', 'r.nid = n.nid AND r.vid = n.vid');
// Add fields and MATCH queries.
$selects = array();
if (db_table_exists('field_data_body')) {
$body_table = $query
->join('field_data_body', 'b', 'n.nid = b.entity_id');
$selects[] = "{$body_table}.body_value";
}
// Add extra fields to the query if enabled.
if (module_exists('field') && variable_get('similar_include_fields', 0) == 1) {
foreach (similar_get_indices() as $table => $indexed) {
if (!empty($indexed) && db_table_exists($table)) {
$alias = $query
->join($table, $table, "n.nid = {$table}.entity_id");
if (count($indexed) > 1) {
$selects[] = "{$alias}." . implode(", {$alias}.", $indexed);
}
elseif (count($indexed) == 1) {
$field = array_pop($indexed);
$selects[] = "{$alias}.{$field}";
}
}
}
}
$expression_params = array();
$expression = "(2 *(MATCH (r.title) AGAINST (:expr_param)))";
$expression_params['expr_param'] = $text;
$param_count = 1;
foreach ($selects as $field) {
$param_count++;
$expression_params['expr_param_' . $param_count] = $text;
$expression .= " + (MATCH ({$field}) AGAINST (:expr_param_{$param_count}))";
}
$expression = "({$expression})";
//$fields = implode(", ", $selects);
//$expression = "MATCH ($fields) AGAINST ('$text')";
$query
->addExpression($expression, 'score', $expression_params);
$query
->condition('n.status', 0, '<>');
$query
->condition('r.nid', $node->nid, '<>');
$query
->condition('n.type', $types, 'IN');
$query
->groupBy('n.nid');
$query
->having('score > 0');
$query
->orderBy('score', 'DESC');
$query
->range(0, variable_get('similar_num_display', 5));
$query
->addTag('node_access');
// Add taxonomy filter if enabled.
if (module_exists('taxonomy') && (variable_get('similar_taxonomy_filter', 0) == 2 && ($taxonomy_tids = variable_get('similar_taxonomy_tids', array()))) || variable_get('similar_taxonomy_filter', 0) == 1 && ($taxonomy_tids = _similar_taxonomy_membership($node->nid))) {
array_walk($taxonomy_tids, '_similar_force_int');
if (sizeof($taxonomy_tids) > 1) {
$taxonomy_tids = implode(',', $taxonomy_tids);
}
else {
list(, $taxonomy_tids) = each($taxonomy_tids);
$taxonomy_tids = (int) $taxonomy_tids;
}
$query
->join('taxonomy_index', 'i', 'r.nid = i.nid AND i.tid IN (:tids)', array(
':tids' => $taxonomy_tids,
));
}
$result = $query
->execute()
->fetchAll();
foreach ($result as $node) {
$content = node_load($node->nid);
$no_follow = variable_get('similar_rel_nofollow', 0) ? array(
'rel' => 'nofollow',
) : array();
if ($summary) {
$items[] = '<div class="similar-title">' . l($content->title, 'node/' . $node->nid, array(
'attributes' => array(
'title' => $content->title,
) + $no_follow,
'absolute' => TRUE,
)) . '</div><div class="similar-summary">' . check_markup($content->body[$content->language][0]['safe_summary'], $content->body[$content->language][0]['format'], FALSE) . '</div>';
}
else {
$items[] = l($content->title, 'node/' . $node->nid, array(
'attributes' => array(
'title' => $content->title,
) + $no_follow,
));
}
}
return sizeof($items) > 0 ? theme('item_list', array(
'items' => $items,
)) : '';
}
Functions
Name![]() |
Description |
---|---|
similar_block_configure | Implements hook_block_configure(). |
similar_block_info | Implements hook_block_info(). |
similar_block_save | Implements hook_block_save(). |
similar_block_view | Implements hook_block_view(). |
similar_cron | Implements hook_cron(). |
similar_get_indices | Returns data about what fields are currently indexed. |
similar_help | Implements hook_help(). |
similar_theme | Implements hook_theme(). |
theme_similar_content | Queries the database for similar entries and puts them in a HTML list. |
_similar_add_index | Adds FULLTEXT indexes to field value database columns. |
_similar_content_type_escape | Strips characters from node type strings. |
_similar_force_int | Prevents SQL injection be forcing an integer value. |
_similar_published_node_types | Queries for published node types. http://drupal.org/node/33444 |
_similar_taxonomy_membership | Queries for taxonomies to which a specific node belongs. http://drupal.org/node/51041 |
_similar_taxonomy_names | Queries for taxonomy names. http://drupal.org/node/51041 |