Commit 44063e1c authored by Dries's avatar Dries

- Patch #505214 by pwolanin: make the search module a slightly better framework.

parent d4d43f11
...@@ -1249,110 +1249,132 @@ function _node_rankings(SelectQueryExtender $query) { ...@@ -1249,110 +1249,132 @@ function _node_rankings(SelectQueryExtender $query) {
} }
/** /**
* Implement hook_search(). * Implement hook_search_info().
*/ */
function node_search($op = 'search', $keys = NULL) { function node_search_info() {
switch ($op) { return array(
case 'name': 'title' => 'Content',
return t('Content'); 'path' => 'node',
);
case 'reset': }
db_update('search_dataset')
->fields(array('reindex' => REQUEST_TIME))
->condition('type', 'node')
->execute();
return;
case 'status':
$total = db_query('SELECT COUNT(*) FROM {node} WHERE status = 1')->fetchField();
$remaining = db_query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE n.status = 1 AND d.sid IS NULL OR d.reindex <> 0")->fetchField();
return array('remaining' => $remaining, 'total' => $total);
case 'admin':
$form = array();
// Output form for defining rank factor weights.
$form['content_ranking'] = array(
'#type' => 'fieldset',
'#title' => t('Content ranking'),
);
$form['content_ranking']['#theme'] = 'node_search_admin';
$form['content_ranking']['info'] = array(
'#value' => '<em>' . t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '</em>'
);
// Note: reversed to reflect that higher number = higher ranking. /**
$options = drupal_map_assoc(range(0, 10)); * Implement hook_search_access().
foreach (module_invoke_all('ranking') as $var => $values) { */
$form['content_ranking']['factors']['node_rank_' . $var] = array( function node_search_access() {
'#title' => $values['title'], return user_access('access content');
'#type' => 'select', }
'#options' => $options,
'#default_value' => variable_get('node_rank_' . $var, 0),
);
}
return $form;
case 'search': /**
// Build matching conditions * Implement hook_search_reset().
$query = db_select('search_index', 'i')->extend('SearchQuery')->extend('PagerDefault'); */
$query->join('node', 'n', 'n.nid = i.sid'); function node_search_reset() {
$query db_update('search_dataset')
->condition('n.status', 1) ->fields(array('reindex' => REQUEST_TIME))
->addTag('node_access') ->condition('type', 'node')
->searchExpression($keys, 'node'); ->execute();
}
// Insert special keywords.
$query->setOption('type', 'n.type');
$query->setOption('language', 'n.language');
if ($query->setOption('term', 'tn.nid')) {
$query->join('taxonomy_term_node', 'tn', 'n.vid = tn.vid');
}
// Only continue if the first pass query matches.
if (!$query->executeFirstPass()) {
return array();
}
// Add the ranking expressions. /**
_node_rankings($query); * Implement hook_search_status().
*/
function node_search_status() {
$total = db_query('SELECT COUNT(*) FROM {node} WHERE status = 1')->fetchField();
$remaining = db_query("SELECT COUNT(*) FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE n.status = 1 AND d.sid IS NULL OR d.reindex <> 0")->fetchField();
return array('remaining' => $remaining, 'total' => $total);
}
// Add a count query. /**
$inner_query = clone $query; * Implement hook_search_admin().
$count_query = db_select($inner_query->fields('i', array('sid'))); */
$count_query->addExpression('COUNT(*)'); function node_search_admin() {
$query->setCountQuery($count_query); $form = array();
$find = $query // Output form for defining rank factor weights.
->limit(10) $form['content_ranking'] = array(
->execute(); '#type' => 'fieldset',
'#title' => t('Content ranking'),
);
$form['content_ranking']['#theme'] = 'node_search_admin';
$form['content_ranking']['info'] = array(
'#value' => '<em>' . t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '</em>'
);
// Note: reversed to reflect that higher number = higher ranking.
$options = drupal_map_assoc(range(0, 10));
foreach (module_invoke_all('ranking') as $var => $values) {
$form['content_ranking']['factors']['node_rank_' . $var] = array(
'#title' => $values['title'],
'#type' => 'select',
'#options' => $options,
'#default_value' => variable_get('node_rank_' . $var, 0),
);
}
return $form;
}
// Load results. /**
$results = array(); * Implement hook_search_execute().
foreach ($find as $item) { */
// Render the node. function node_search_execute($keys = NULL) {
$node = node_load($item->sid); // Build matching conditions
$node = node_build_content($node, 'search_result'); $query = db_select('search_index', 'i')->extend('SearchQuery')->extend('PagerDefault');
$node->rendered = drupal_render($node->content); $query->join('node', 'n', 'n.nid = i.sid');
$query
// Fetch comments for snippet. ->condition('n.status', 1)
$node->rendered .= ' ' . module_invoke('comment', 'node_update_index', $node); ->addTag('node_access')
// Fetch terms for snippet. ->searchExpression($keys, 'node');
$node->rendered .= ' ' . module_invoke('taxonomy', 'node_update_index', $node);
// Insert special keywords.
$extra = module_invoke_all('node_search_result', $node); $query->setOption('type', 'n.type');
$query->setOption('language', 'n.language');
$results[] = array( if ($query->setOption('term', 'tn.nid')) {
'link' => url('node/' . $item->sid, array('absolute' => TRUE)), $query->join('taxonomy_term_node', 'tn', 'n.vid = tn.vid');
'type' => check_plain(node_type_get_name($node)), }
'title' => $node->title, // Only continue if the first pass query matches.
'user' => theme('username', $node), if (!$query->executeFirstPass()) {
'date' => $node->changed, return array();
'node' => $node, }
'extra' => $extra,
'score' => $item->calculated_score, // Add the ranking expressions.
'snippet' => search_excerpt($keys, $node->rendered), _node_rankings($query);
);
} // Add a count query.
return $results; $inner_query = clone $query;
$count_query = db_select($inner_query->fields('i', array('sid')));
$count_query->addExpression('COUNT(*)');
$query->setCountQuery($count_query);
$find = $query
->limit(10)
->execute();
// Load results.
$results = array();
foreach ($find as $item) {
// Render the node.
$node = node_load($item->sid);
$node = node_build_content($node, 'search_result');
$node->rendered = drupal_render($node->content);
// Fetch comments for snippet.
$node->rendered .= ' ' . module_invoke('comment', 'node_update_index', $node);
// Fetch terms for snippet.
$node->rendered .= ' ' . module_invoke('taxonomy', 'node_update_index', $node);
$extra = module_invoke_all('node_search_result', $node);
$results[] = array(
'link' => url('node/' . $item->sid, array('absolute' => TRUE)),
'type' => check_plain(node_type_get_name($node)),
'title' => $node->title,
'user' => theme('username', $node),
'date' => $node->changed,
'node' => $node,
'extra' => $extra,
'score' => $item->calculated_score,
'snippet' => search_excerpt($keys, $node->rendered),
);
} }
return $results;
} }
/** /**
......
...@@ -26,6 +26,27 @@ function search_reindex_confirm_submit(&$form, &$form_state) { ...@@ -26,6 +26,27 @@ function search_reindex_confirm_submit(&$form, &$form_state) {
} }
} }
/**
* Helper function to get real module names.
*/
function _search_get_module_names() {
$search_info = search_get_info();
$modules = db_select('system', 's')
->fields('s', array('name', 'info'))
->condition('s.status', 1)
->condition('s.type', 'module')
->condition('s.name', array_keys($search_info), 'IN')
->orderBy('s.name')
->execute();
$names = array();
foreach ($modules as $item) {
$info = unserialize($item->info);
$names[$item->name] = $info['name'];
}
return $names;
}
/** /**
* Menu callback; displays the search module settings page. * Menu callback; displays the search module settings page.
* *
...@@ -38,12 +59,13 @@ function search_admin_settings() { ...@@ -38,12 +59,13 @@ function search_admin_settings() {
// Collect some stats // Collect some stats
$remaining = 0; $remaining = 0;
$total = 0; $total = 0;
foreach (module_implements('search') as $module) { foreach(variable_get('search_active_modules', array('node', 'user')) as $module) {
$function = $module . '_search'; if ($status = module_invoke($module, 'search_status')) {
$status = $function('status'); $remaining += $status['remaining'];
$remaining += $status['remaining']; $total += $status['total'];
$total += $status['total']; }
} }
$count = format_plural($remaining, 'There is 1 item left to index.', 'There are @count items left to index.'); $count = format_plural($remaining, 'There is 1 item left to index.', 'There are @count items left to index.');
$percentage = ((int)min(100, 100 * ($total - $remaining) / max(1, $total))) . '%'; $percentage = ((int)min(100, 100 * ($total - $remaining) / max(1, $total))) . '%';
$status = '<p><strong>' . t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) . ' ' . $count . '</strong></p>'; $status = '<p><strong>' . t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) . ' ' . $count . '</strong></p>';
...@@ -88,10 +110,24 @@ function search_admin_settings() { ...@@ -88,10 +110,24 @@ function search_admin_settings() {
'#description' => t('Whether to apply a simple Chinese/Japanese/Korean tokenizer based on overlapping sequences. Turn this off if you want to use an external preprocessor for this instead. Does not affect other languages.') '#description' => t('Whether to apply a simple Chinese/Japanese/Korean tokenizer based on overlapping sequences. Turn this off if you want to use an external preprocessor for this instead. Does not affect other languages.')
); );
$form['search_active_modules'] = array(
'#type' => 'checkboxes',
'#title' => t('Active search modules'),
'#default_value' => array('node', 'user'),
'#options' => _search_get_module_names(),
'#description' => t('Determine which search modules are active from the available modules.')
);
$form['#submit'][] = 'search_admin_settings_submit'; $form['#submit'][] = 'search_admin_settings_submit';
// Per module settings // Per module settings
$form = array_merge($form, module_invoke_all('search', 'admin')); foreach(variable_get('search_active_modules', array('node', 'user')) as $module) {
$added_form = module_invoke($module, 'search_admin');
if (is_array($added_form)) {
$form = array_merge($form, $added_form);
}
}
return system_settings_form($form, TRUE); return system_settings_form($form, TRUE);
} }
...@@ -105,6 +141,18 @@ function search_admin_settings_submit($form, &$form_state) { ...@@ -105,6 +141,18 @@ function search_admin_settings_submit($form, &$form_state) {
drupal_set_message(t('The index will be rebuilt.')); drupal_set_message(t('The index will be rebuilt.'));
search_reindex(); search_reindex();
} }
$current_modules = variable_get('search_active_modules', array('node', 'user'));
// Check whether we are resetting the values.
if ($form_state['clicked_button']['#value'] == t('Reset to defaults')) {
$new_modules = array('node', 'user');
}
else {
$new_modules = array_filter($form_state['values']['search_active_modules']);
}
if (array_diff($current_modules, $new_modules)) {
drupal_set_message(t('The active search modules have been changed.'));
variable_set('menu_rebuild_needed', TRUE);
}
} }
/** /**
......
This diff is collapsed.
...@@ -218,25 +218,45 @@ function search_menu() { ...@@ -218,25 +218,45 @@ function search_menu() {
'file path' => drupal_get_path('module', 'dblog'), 'file path' => drupal_get_path('module', 'dblog'),
'file' => 'dblog.admin.inc', 'file' => 'dblog.admin.inc',
); );
drupal_static_reset('search_get_info');
foreach (module_implements('search') as $module) { $search_hooks = search_get_info();
$items['search/' . $module . '/%menu_tail'] = array( foreach(variable_get('search_active_modules', array('node', 'user')) as $module) {
'title callback' => 'module_invoke', if (isset($search_hooks[$module])) {
'title arguments' => array($module, 'search', 'name', TRUE), $items['search/' . $search_hooks[$module]['path'] . '/%menu_tail'] = array(
'page callback' => 'search_view', 'title' => $search_hooks[$module]['title'],
'page arguments' => array($module), 'page callback' => 'search_view',
'access callback' => '_search_menu', 'page arguments' => array($module),
'access arguments' => array($module), 'access callback' => '_search_menu_access',
'type' => MENU_LOCAL_TASK, 'access arguments' => array($module),
'parent' => 'search', 'type' => MENU_LOCAL_TASK,
'file' => 'search.pages.inc', 'file' => 'search.pages.inc',
); );
}
} }
return $items; return $items;
} }
function _search_menu($name) { /**
return user_access('search content') && module_invoke($name, 'search', 'name'); * Get information about all available search hooks.
*/
function search_get_info() {
$search_hooks = &drupal_static(__FUNCTION__);
if (!isset($search_hooks)) {
foreach (module_implements('search_info') as $module) {
$search_hooks[$module] = call_user_func($module . '_search_info');
// Use module name as the default.
$search_hooks[$module] += array('title' => $module, 'path' => $module);
}
}
return $search_hooks;
}
/**
* Access callback for search tabs.
*/
function _search_menu_access($name) {
return user_access('search content') && (!function_exists($name . '_search_access') || module_invoke($name, 'search_access'));
} }
/** /**
...@@ -297,8 +317,10 @@ function search_cron() { ...@@ -297,8 +317,10 @@ function search_cron() {
// to date. // to date.
register_shutdown_function('search_update_totals'); register_shutdown_function('search_update_totals');
// Update word index foreach(variable_get('search_active_modules', array('node', 'user')) as $module) {
module_invoke_all('update_index'); // Update word index
module_invoke($module, 'update_index');
}
} }
/** /**
...@@ -693,6 +715,10 @@ function search_touch_node($nid) { ...@@ -693,6 +715,10 @@ function search_touch_node($nid) {
->execute(); ->execute();
} }
function search_touch_data($type, $sid) {
db_query("UPDATE {search_dataset} SET reindex = :time WHERE sid = :sid AND type = :type", array(':time' => REQUEST_TIME, ':sid' => $sid, ':type' => $type));
}
/** /**
* Implement hook_node_update_index(). * Implement hook_node_update_index().
*/ */
...@@ -969,8 +995,8 @@ function template_preprocess_search_block_form(&$variables) { ...@@ -969,8 +995,8 @@ function template_preprocess_search_block_form(&$variables) {
function search_data($keys = NULL, $type = 'node') { function search_data($keys = NULL, $type = 'node') {
if (isset($keys)) { if (isset($keys)) {
if (module_hook($type, 'search')) { if (module_hook($type, 'search_execute')) {
$results = module_invoke($type, 'search', 'search', $keys); $results = module_invoke($type, 'search_execute', $keys);
if (isset($results) && is_array($results) && count($results)) { if (isset($results) && is_array($results) && count($results)) {
if (module_hook($type, 'search_page')) { if (module_hook($type, 'search_page')) {
return module_invoke($type, 'search_page', $results); return module_invoke($type, 'search_page', $results);
......
...@@ -374,7 +374,7 @@ class SearchRankingTestCase extends DrupalWebTestCase { ...@@ -374,7 +374,7 @@ class SearchRankingTestCase extends DrupalWebTestCase {
} }
// Do the search and assert the results. // Do the search and assert the results.
$set = node_search('search', 'rocks'); $set = node_search_execute('rocks');
$this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.'); $this->assertEqual($set[0]['node']->nid, $nodes[$node_rank][1]->nid, 'Search ranking "' . $node_rank . '" order.');
} }
} }
......
...@@ -795,39 +795,46 @@ function user_file_delete($file) { ...@@ -795,39 +795,46 @@ function user_file_delete($file) {
} }
/** /**
* Implement hook_search(). * Implement hook_search_info().
*/ */
function user_search($op = 'search', $keys = NULL, $skip_access_check = FALSE) { function user_search_info() {
switch ($op) { return array(
case 'name': 'title' => 'Users',
if ($skip_access_check || user_access('access user profiles')) { );
return t('Users'); }
}
case 'search': /**
if (user_access('access user profiles')) { * Implement hook_search_access().
$find = array(); */
// Replace wildcards with MySQL/PostgreSQL wildcards. function user_search_access() {
$keys = preg_replace('!\*+!', '%', $keys); return user_access('access user profiles');
$query = db_select('users')->extend('PagerDefault'); }
$query->fields('users', array('name', 'uid', 'mail'));
if (user_access('administer users')) { /**
// Administrators can also search in the otherwise private email field. * Implement hook_search_execute().
$query->condition(db_or()-> */
where('LOWER(name) LIKE LOWER(:name)', array(':name' => "%$keys%"))-> function user_search_execute($keys = NULL) {
where('LOWER(mail) LIKE LOWER(:mail)', array(':mail' => "%$keys%"))); $find = array();
} // Replace wildcards with MySQL/PostgreSQL wildcards.
else { $keys = preg_replace('!\*+!', '%', $keys);
$query->where('LOWER(name) LIKE LOWER(:name)', array(':name' => "%$keys%")); $query = db_select('users')->extend('PagerDefault');
} $query->fields('users', array('name', 'uid', 'mail'));
$result = $query if (user_access('administer users')) {
->limit(15) // Administrators can also search in the otherwise private email field.
->execute(); $query->condition(db_or()->
foreach ($result as $account) { where('LOWER(name) LIKE LOWER(:name)', array(':name' => "%$keys%"))->
$find[] = array('title' => $account->name . ' (' . $account->mail . ')', 'link' => url('user/' . $account->uid, array('absolute' => TRUE))); where('LOWER(mail) LIKE LOWER(:mail)', array(':mail' => "%$keys%")));
} }
return $find; else {
} $query->where('LOWER(name) LIKE LOWER(:name)', array(':name' => "%$keys%"));
}
$result = $query
->limit(15)
->execute();
foreach ($result as $account) {
$find[] = array('title' => $account->name . ' (' . $account->mail . ')', 'link' => url('user/' . $account->uid, array('absolute' => TRUE)));
} }
return $find;
} }
/** /**
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment