? modules/sql_search ? modules/sql_search.tar.gz ? sites/default/files ? sites/default/settings.php Index: modules/node/node.info =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.info,v retrieving revision 1.6 diff -u -p -r1.6 node.info --- modules/node/node.info 6 May 2008 12:18:48 -0000 1.6 +++ modules/node/node.info 13 Jul 2008 20:55:30 -0000 @@ -8,3 +8,4 @@ files[] = node.module files[] = content_types.inc files[] = node.admin.inc files[] = node.pages.inc +files[] = node.sql_search.inc Index: modules/node/node.module =================================================================== RCS file: /cvs/drupal/drupal/modules/node/node.module,v retrieving revision 1.967 diff -u -p -r1.967 node.module --- modules/node/node.module 26 May 2008 17:12:55 -0000 1.967 +++ modules/node/node.module 13 Jul 2008 20:55:32 -0000 @@ -1202,138 +1202,6 @@ function _node_rankings() { return $rankings; } - -/** - * Implementation of hook_search(). - */ -function node_search($op = 'search', $keys = NULL) { - switch ($op) { - case 'name': - return t('Content'); - - case 'reset': - db_query("UPDATE {search_dataset} SET reindex = %d WHERE type = 'node'", time()); - return; - - case 'status': - $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1')); - $remaining = db_result(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")); - 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' => '' . 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.') . '' - ); - - // 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; - - case 'search': - // Build matching conditions - list($join1, $where1) = _db_rewrite_sql(); - $arguments1 = array(); - $conditions1 = 'n.status = 1'; - - if ($type = search_query_extract($keys, 'type')) { - $types = array(); - foreach (explode(',', $type) as $t) { - $types[] = "n.type = '%s'"; - $arguments1[] = $t; - } - $conditions1 .= ' AND (' . implode(' OR ', $types) . ')'; - $keys = search_query_insert($keys, 'type'); - } - - if ($category = search_query_extract($keys, 'category')) { - $categories = array(); - foreach (explode(',', $category) as $c) { - $categories[] = "tn.tid = %d"; - $arguments1[] = $c; - } - $conditions1 .= ' AND (' . implode(' OR ', $categories) . ')'; - $join1 .= ' INNER JOIN {term_node} tn ON n.vid = tn.vid'; - $keys = search_query_insert($keys, 'category'); - } - - if ($languages = search_query_extract($keys, 'language')) { - $categories = array(); - foreach (explode(',', $languages) as $l) { - $categories[] = "n.language = '%s'"; - $arguments1[] = $l; - } - $conditions1 .= ' AND (' . implode(' OR ', $categories) . ')'; - $keys = search_query_insert($keys, 'language'); - } - - // Get the ranking expressions. - $rankings = _node_rankings(); - - // When all search factors are disabled (ie they have a weight of zero), - // The default score is based only on keyword relevance. - if ($rankings['total'] == 0) { - $total = 1; - $arguments2 = array(); - $join2 = ''; - $select2 = 'i.relevance AS score'; - } - else { - $total = $rankings['total']; - $arguments2 = $rankings['arguments']; - $join2 = implode(' ', $rankings['join']); - $select2 = '('. implode(' + ', $rankings['score']) .') AS score'; - } - - // Do search. - $find = do_search($keys, 'node', 'INNER JOIN {node} n ON n.nid = i.sid ' . $join1, $conditions1 . (empty($where1) ? '' : ' AND ' . $where1), $arguments1, $select2, $join2, $arguments2); - - // Load results. - $results = array(); - foreach ($find as $item) { - // Build the node body. - $node = node_load($item->sid); - $node->build_mode = NODE_BUILD_SEARCH_RESULT; - $node = node_build_content($node, FALSE, FALSE); - $node->body = drupal_render($node->content); - - // Fetch comments for snippet. - $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index'); - // Fetch terms for snippet. - $node->body .= module_invoke('taxonomy', 'nodeapi', $node, 'update index'); - - $extra = node_invoke_nodeapi($node, 'search result'); - - $results[] = array( - 'link' => url('node/' . $item->sid, array('absolute' => TRUE)), - 'type' => check_plain(node_get_types('name', $node)), - 'title' => $node->title, - 'user' => theme('username', $node), - 'date' => $node->changed, - 'node' => $node, - 'extra' => $extra, - 'score' => $total ? ($item->score / $total) : 0, - 'snippet' => search_excerpt($keys, $node->body), - ); - } - return $results; - } -} - /** * Implementation of hook_ranking(). */ @@ -1649,6 +1517,9 @@ function node_page_title($node) { */ function node_init() { drupal_add_css(drupal_get_path('module', 'node') . '/node.css'); + if (module_exists('sql_search')) { + module_load_include('inc', 'node', 'node.sql_search'); + } } function node_last_changed($nid) { @@ -1825,178 +1696,6 @@ function node_page_view($node, $cid = NU } /** - * Implementation of hook_update_index(). - */ -function node_update_index() { - $limit = (int)variable_get('search_cron_limit', 100); - - // Store the maximum possible comments per thread (used for ranking by reply count) - variable_set('node_cron_comments_scale', 1.0 / max(1, db_result(db_query('SELECT MAX(comment_count) FROM {node_comment_statistics}')))); - variable_set('node_cron_views_scale', 1.0 / max(1, db_result(db_query('SELECT MAX(totalcount) FROM {node_counter}')))); - - $result = db_query_range("SELECT n.nid FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, n.nid ASC", 0, $limit); - - while ($node = db_fetch_object($result)) { - _node_index_node($node); - } -} - -/** - * Index a single node. - * - * @param $node - * The node to index. - */ -function _node_index_node($node) { - $node = node_load($node->nid); - - // save the changed time of the most recent indexed node, for the search results half-life calculation - variable_set('node_cron_last', $node->changed); - - // Build the node body. - $node->build_mode = NODE_BUILD_SEARCH_INDEX; - $node = node_build_content($node, FALSE, FALSE); - $node->body = drupal_render($node->content); - - $text = '

' . check_plain($node->title) . '

' . $node->body; - - // Fetch extra data normally not visible - $extra = node_invoke_nodeapi($node, 'update index'); - foreach ($extra as $t) { - $text .= $t; - } - - // Update index - search_index($node->nid, 'node', $text); -} - -/** - * Implementation of hook_form_alter(). - */ -function node_form_alter(&$form, $form_state, $form_id) { - // Advanced node search form - if ($form_id == 'search_form' && $form['module']['#value'] == 'node' && user_access('use advanced search')) { - // Keyword boxes: - $form['advanced'] = array( - '#type' => 'fieldset', - '#title' => t('Advanced search'), - '#collapsible' => TRUE, - '#collapsed' => TRUE, - '#attributes' => array('class' => 'search-advanced'), - ); - $form['advanced']['keywords'] = array( - '#prefix' => '
', - '#suffix' => '
', - ); - $form['advanced']['keywords']['or'] = array( - '#type' => 'textfield', - '#title' => t('Containing any of the words'), - '#size' => 30, - '#maxlength' => 255, - ); - $form['advanced']['keywords']['phrase'] = array( - '#type' => 'textfield', - '#title' => t('Containing the phrase'), - '#size' => 30, - '#maxlength' => 255, - ); - $form['advanced']['keywords']['negative'] = array( - '#type' => 'textfield', - '#title' => t('Containing none of the words'), - '#size' => 30, - '#maxlength' => 255, - ); - - // Taxonomy box: - if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) { - $form['advanced']['category'] = array( - '#type' => 'select', - '#title' => t('Only in the category(s)'), - '#prefix' => '
', - '#size' => 10, - '#suffix' => '
', - '#options' => $taxonomy, - '#multiple' => TRUE, - ); - } - - // Node types: - $types = array_map('check_plain', node_get_types('names')); - $form['advanced']['type'] = array( - '#type' => 'checkboxes', - '#title' => t('Only of the type(s)'), - '#prefix' => '
', - '#suffix' => '
', - '#options' => $types, - ); - $form['advanced']['submit'] = array( - '#type' => 'submit', - '#value' => t('Advanced search'), - '#prefix' => '
', - '#suffix' => '
', - ); - - // Languages: - $language_options = array(); - foreach (language_list('language') as $key => $object) { - $language_options[$key] = $object->name; - } - if (count($language_options) > 1) { - $form['advanced']['language'] = array( - '#type' => 'checkboxes', - '#title' => t('Languages'), - '#prefix' => '
', - '#suffix' => '
', - '#options' => $language_options, - ); - } - - - $form['#validate'][] = 'node_search_validate'; - } -} - -/** - * Form API callback for the search form. Registered in node_form_alter(). - */ -function node_search_validate($form, &$form_state) { - // Initialise using any existing basic search keywords. - $keys = $form_state['values']['processed_keys']; - - // Insert extra restrictions into the search keywords string. - if (isset($form_state['values']['type']) && is_array($form_state['values']['type'])) { - // Retrieve selected types - Forms API sets the value of unselected checkboxes to 0. - $form_state['values']['type'] = array_filter($form_state['values']['type']); - if (count($form_state['values']['type'])) { - $keys = search_query_insert($keys, 'type', implode(',', array_keys($form_state['values']['type']))); - } - } - - if (isset($form_state['values']['category']) && is_array($form_state['values']['category'])) { - $keys = search_query_insert($keys, 'category', implode(',', $form_state['values']['category'])); - } - if (isset($form_state['values']['language']) && is_array($form_state['values']['language'])) { - $keys = search_query_insert($keys, 'language', implode(',', array_filter($form_state['values']['language']))); - } - if ($form_state['values']['or'] != '') { - if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['or'], $matches)) { - $keys .= ' ' . implode(' OR ', $matches[1]); - } - } - if ($form_state['values']['negative'] != '') { - if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['negative'], $matches)) { - $keys .= ' -' . implode(' -', $matches[1]); - } - } - if ($form_state['values']['phrase'] != '') { - $keys .= ' "' . str_replace('"', ' ', $form_state['values']['phrase']) . '"'; - } - if (!empty($keys)) { - form_set_value($form['basic']['inline']['processed_keys'], trim($keys), $form_state); - } -} - -/** * @defgroup node_access Node access rights * @{ * The node access system determines who can do what to which nodes. Index: modules/node/node.sql_search.inc =================================================================== RCS file: modules/node/node.sql_search.inc diff -N modules/node/node.sql_search.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/node/node.sql_search.inc 13 Jul 2008 20:55:32 -0000 @@ -0,0 +1,298 @@ + t('Content'), + 'access' => 'access content', + ); + + case 'reset': + db_query("UPDATE {search_dataset} SET reindex = %d WHERE type = 'node'", time()); + return; + + case 'wipe': + db_query("DELETE FROM {search_index} WHERE sid = %d AND type = '%s'", $sid, $type); + // Don't remove links if re-indexing. + if (!$reindex) { + db_query("DELETE FROM {search_node_links} WHERE sid = %d AND type = '%s'", $sid, $type); + } + return; + case 'status': + $total = db_result(db_query('SELECT COUNT(*) FROM {node} WHERE status = 1')); + $remaining = db_result(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")); + 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' => '' . 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.') . '' + ); + + // 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; + + case 'search': + // Build matching conditions + list($join1, $where1) = _db_rewrite_sql(); + $arguments1 = array(); + $conditions1 = 'n.status = 1'; + + if ($type = search_query_extract($keys, 'type')) { + $types = array(); + foreach (explode(',', $type) as $t) { + $types[] = "n.type = '%s'"; + $arguments1[] = $t; + } + $conditions1 .= ' AND (' . implode(' OR ', $types) . ')'; + $keys = search_query_insert($keys, 'type'); + } + + if ($category = search_query_extract($keys, 'category')) { + $categories = array(); + foreach (explode(',', $category) as $c) { + $categories[] = "tn.tid = %d"; + $arguments1[] = $c; + } + $conditions1 .= ' AND (' . implode(' OR ', $categories) . ')'; + $join1 .= ' INNER JOIN {term_node} tn ON n.vid = tn.vid'; + $keys = search_query_insert($keys, 'category'); + } + + if ($languages = search_query_extract($keys, 'language')) { + $categories = array(); + foreach (explode(',', $languages) as $l) { + $categories[] = "n.language = '%s'"; + $arguments1[] = $l; + } + $conditions1 .= ' AND (' . implode(' OR ', $categories) . ')'; + $keys = search_query_insert($keys, 'language'); + } + + // Get the ranking expressions. + $rankings = _node_rankings(); + + // When all search factors are disabled (ie they have a weight of zero), + // The default score is based only on keyword relevance. + if ($rankings['total'] == 0) { + $total = 1; + $arguments2 = array(); + $join2 = ''; + $select2 = 'i.relevance AS score'; + } + else { + $total = $rankings['total']; + $arguments2 = $rankings['arguments']; + $join2 = implode(' ', $rankings['join']); + $select2 = '('. implode(' + ', $rankings['score']) .') AS score'; + } + + // Do search. + $find = sql_search_do_search($keys, 'node', 'INNER JOIN {node} n ON n.nid = i.sid ' . $join1, $conditions1 . (empty($where1) ? '' : ' AND ' . $where1), $arguments1, $select2, $join2, $arguments2); + + // Load results. + $results = array(); + foreach ($find as $item) { + // Build the node body. + $node = node_load($item->sid); + $node->build_mode = NODE_BUILD_SEARCH_RESULT; + $node = node_build_content($node, FALSE, FALSE); + $node->body = drupal_render($node->content); + + // Fetch comments for snippet. + $node->body .= module_invoke('comment', 'nodeapi', $node, 'update index'); + // Fetch terms for snippet. + $node->body .= module_invoke('taxonomy', 'nodeapi', $node, 'update index'); + + $extra = node_invoke_nodeapi($node, 'search result'); + + $results[] = array( + 'link' => url('node/' . $item->sid, array('absolute' => TRUE)), + 'type' => check_plain(node_get_types('name', $node)), + 'title' => $node->title, + 'user' => theme('username', $node), + 'date' => $node->changed, + 'node' => $node, + 'extra' => $extra, + 'score' => $total ? ($item->score / $total) : 0, + 'snippet' => search_excerpt($keys, $node->body), + ); + } + return $results; + } +} + +/** + * Implementation of hook_form_alter(). + */ +function sql_search_form_search_form_alter(&$form, $form_state) { + // Advanced node search form + if (variable_get('search_handler', _search_default_handler()) == 'sql_search' && $form['search_type']['#value'] == 'node' && user_access('use advanced search')) { + // Keyword boxes: + $form['advanced'] = array( + '#type' => 'fieldset', + '#title' => t('Advanced search'), + '#collapsible' => TRUE, + '#collapsed' => TRUE, + '#attributes' => array('class' => 'search-advanced'), + ); + $form['advanced']['keywords'] = array( + '#prefix' => '
', + '#suffix' => '
', + ); + $form['advanced']['keywords']['or'] = array( + '#type' => 'textfield', + '#title' => t('Containing any of the words'), + '#size' => 30, + '#maxlength' => 255, + ); + $form['advanced']['keywords']['phrase'] = array( + '#type' => 'textfield', + '#title' => t('Containing the phrase'), + '#size' => 30, + '#maxlength' => 255, + ); + $form['advanced']['keywords']['negative'] = array( + '#type' => 'textfield', + '#title' => t('Containing none of the words'), + '#size' => 30, + '#maxlength' => 255, + ); + + // Taxonomy box: + if ($taxonomy = module_invoke('taxonomy', 'form_all', 1)) { + $form['advanced']['category'] = array( + '#type' => 'select', + '#title' => t('Only in the category(s)'), + '#prefix' => '
', + '#size' => 10, + '#suffix' => '
', + '#options' => $taxonomy, + '#multiple' => TRUE, + ); + } + + // Node types: + $types = array_map('check_plain', node_get_types('names')); + $form['advanced']['type'] = array( + '#type' => 'checkboxes', + '#title' => t('Only of the type(s)'), + '#prefix' => '
', + '#suffix' => '
', + '#options' => $types, + ); + $form['advanced']['submit'] = array( + '#type' => 'submit', + '#value' => t('Advanced search'), + '#prefix' => '
', + '#suffix' => '
', + ); + + // Languages: + $language_options = array(); + foreach (language_list('language') as $key => $object) { + $language_options[$key] = $object->name; + } + if (count($language_options) > 1) { + $form['advanced']['language'] = array( + '#type' => 'checkboxes', + '#title' => t('Languages'), + '#prefix' => '
', + '#suffix' => '
', + '#options' => $language_options, + ); + } + + + $form['#validate'][] = 'node_advanced_search_validate'; + } +} + +/** + * Form API callback for the search form. Registered in node_form_alter(). + */ +function node_advanced_search_validate($form, &$form_state) { + // Initialise using any existing basic search keywords. + $keys = $form_state['values']['processed_keys']; + + // Insert extra restrictions into the search keywords string. + if (isset($form_state['values']['type']) && is_array($form_state['values']['type'])) { + // Retrieve selected types - Forms API sets the value of unselected checkboxes to 0. + $form_state['values']['type'] = array_filter($form_state['values']['type']); + if (count($form_state['values']['type'])) { + $keys = search_query_insert($keys, 'type', implode(',', array_keys($form_state['values']['type']))); + } + } + + if (isset($form_state['values']['category']) && is_array($form_state['values']['category'])) { + $keys = search_query_insert($keys, 'category', implode(',', $form_state['values']['category'])); + } + if (isset($form_state['values']['language']) && is_array($form_state['values']['language'])) { + $keys = search_query_insert($keys, 'language', implode(',', array_filter($form_state['values']['language']))); + } + if ($form_state['values']['or'] != '') { + if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['or'], $matches)) { + $keys .= ' ' . implode(' OR ', $matches[1]); + } + } + if ($form_state['values']['negative'] != '') { + if (preg_match_all('/ ("[^"]+"|[^" ]+)/i', ' ' . $form_state['values']['negative'], $matches)) { + $keys .= ' -' . implode(' -', $matches[1]); + } + } + if ($form_state['values']['phrase'] != '') { + $keys .= ' "' . str_replace('"', ' ', $form_state['values']['phrase']) . '"'; + } + if (!empty($keys)) { + form_set_value($form['basic']['inline']['processed_keys'], trim($keys), $form_state); + } +} + +/** + * Index a single node. + * + * @param $node + * The node to index. + */ +function _node_index($node) { + $node = node_load($node->nid); + + // save the changed time of the most recent indexed node, for the search results half-life calculation + variable_set('node_cron_last', $node->changed); + + // Build the node body. + $node->build_mode = NODE_BUILD_SEARCH_INDEX; + $node = node_build_content($node, FALSE, FALSE); + $node->body = drupal_render($node->content); + + $text = '

' . check_plain($node->title) . '

' . $node->body; + + // Fetch extra data normally not visible + $extra = node_invoke_nodeapi($node, 'update index'); + foreach ($extra as $t) { + $text .= $t; + } + + // Update index + sql_search_index($node->nid, 'node', $text); +} + Index: modules/search/search.admin.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/search/search.admin.inc,v retrieving revision 1.5 diff -u -p -r1.5 search.admin.inc --- modules/search/search.admin.inc 14 Apr 2008 17:48:41 -0000 1.5 +++ modules/search/search.admin.inc 13 Jul 2008 20:55:32 -0000 @@ -7,26 +7,6 @@ */ /** - * Menu callback: confirm wiping of the index. - */ -function search_wipe_confirm() { - return confirm_form(array(), t('Are you sure you want to re-index the site?'), - 'admin/settings/search', t(' The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed. This action cannot be undone.'), t('Re-index site'), t('Cancel')); -} - -/** - * Handler for wipe confirmation - */ -function search_wipe_confirm_submit(&$form, &$form_state) { - if ($form['confirm']) { - search_wipe(); - drupal_set_message(t('The index will be rebuilt.')); - $form_state['redirect'] = 'admin/settings/search'; - return; - } -} - -/** * Menu callback; displays the search module settings page. * * @ingroup forms @@ -34,52 +14,30 @@ function search_wipe_confirm_submit(&$fo * @see search_admin_settings_validate() */ function search_admin_settings() { - // Collect some stats - $remaining = 0; - $total = 0; - foreach (module_list() as $module) { - if (module_hook($module, 'search')) { - $status = module_invoke($module, 'search', 'status'); - $remaining += $status['remaining']; - $total += $status['total']; - } + $form = array(); + $handlers = search_handlers(); + // Return with an error message if there are no available types. + if (empty($handlers)) { + drupal_set_message(t('No search handlers found. To enable search, you must first install and enable a search handler module.'), 'warning'); + return array(); } - $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))) . '%'; - $status = '

' . t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) . ' ' . $count . '

'; - $form['status'] = array('#type' => 'fieldset', '#title' => t('Indexing status')); - $form['status']['status'] = array('#value' => $status); - $form['status']['wipe'] = array('#type' => 'submit', '#value' => t('Re-index site')); - - $items = drupal_map_assoc(array(10, 20, 50, 100, 200, 500)); - - // Indexing throttle: - $form['indexing_throttle'] = array('#type' => 'fieldset', '#title' => t('Indexing throttle')); - $form['indexing_throttle']['search_cron_limit'] = array('#type' => 'select', '#title' => t('Number of items to index per cron run'), '#default_value' => variable_get('search_cron_limit', 100), '#options' => $items, '#description' => t('The maximum number of items indexed in each pass of a cron maintenance task. If necessary, reduce the number of items to prevent timeouts and memory errors while indexing.', array('@cron' => url('admin/reports/status')))); - // Indexing settings: - $form['indexing_settings'] = array('#type' => 'fieldset', '#title' => t('Indexing settings')); - $form['indexing_settings']['info'] = array('#value' => t('

Changing the settings below will cause the site index to be rebuilt. The search index is not cleared but systematically updated to reflect the new settings. Searching will continue to work but new content won\'t be indexed until all existing content has been re-indexed.

The default settings should be appropriate for the majority of sites.

')); - $form['indexing_settings']['minimum_word_size'] = array('#type' => 'textfield', '#title' => t('Minimum word length to index'), '#default_value' => variable_get('minimum_word_size', 3), '#size' => 5, '#maxlength' => 3, '#description' => t('The number of characters a word has to be to be indexed. A lower setting means better search result ranking, but also a larger database. Each search query must contain at least one keyword that is this size (or longer).')); - $form['indexing_settings']['overlap_cjk'] = array('#type' => 'checkbox', '#title' => t('Simple CJK handling'), '#default_value' => variable_get('overlap_cjk', TRUE), '#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['#validate'] = array('search_admin_settings_validate'); - - // Per module settings - $form = array_merge($form, module_invoke_all('search', 'admin')); - return system_settings_form($form); + foreach ($handlers as $type => $values) { + $options[$type] = $values['title']; + } + $form['search_handler'] = array( + '#type' => 'select', + '#title' => t('Search handler'), + '#description' => t('Select your preferred search handler.'), + '#default_value' => variable_get('search_handler', _search_default_handler()), + '#options' => $options, + ); + + $form = system_settings_form($form); + $form['#submit'][] = 'search_admin_settings_menu_rebuild'; + return $form; } -/** - * Validate callback. - */ -function search_admin_settings_validate($form, &$form_state) { - if ($form_state['values']['op'] == t('Re-index site')) { - drupal_goto('admin/settings/search/wipe'); - } - // If these settings change, the index needs to be rebuilt. - if ((variable_get('minimum_word_size', 3) != $form_state['values']['minimum_word_size']) || - (variable_get('overlap_cjk', TRUE) != $form_state['values']['overlap_cjk'])) { - drupal_set_message(t('The index will be rebuilt.')); - search_wipe(); - } +function search_admin_settings_menu_rebuild() { + menu_rebuild(); } + Index: modules/search/search.install =================================================================== RCS file: modules/search/search.install diff -N modules/search/search.install --- modules/search/search.install 15 Mar 2008 12:31:29 -0000 1.15 +++ /dev/null 1 Jan 1970 00:00:00 -0000 @@ -1,158 +0,0 @@ - t('Stores items that will be searched.'), - 'fields' => array( - 'sid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => t('Search item ID, e.g. node ID for nodes.'), - ), - 'type' => array( - 'type' => 'varchar', - 'length' => 16, - 'not null' => FALSE, - 'description' => t('Type of item, e.g. node.'), - ), - 'data' => array( - 'type' => 'text', - 'not null' => TRUE, - 'size' => 'big', - 'description' => t('List of space-separated words from the item.'), - ), - 'reindex' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => t('Set to force node reindexing.'), - ), - ), - 'unique keys' => array( - 'sid_type' => array('sid', 'type'), - ), - ); - - $schema['search_index'] = array( - 'description' => t('Stores the search index, associating words, items and scores.'), - 'fields' => array( - 'word' => array( - 'type' => 'varchar', - 'length' => 50, - 'not null' => TRUE, - 'default' => '', - 'description' => t('The {search_total}.word that is associated with the search item.'), - ), - 'sid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => t('The {search_dataset}.sid of the searchable item to which the word belongs.'), - ), - 'type' => array( - 'type' => 'varchar', - 'length' => 16, - 'not null' => FALSE, - 'description' => t('The {search_dataset}.type of the searchable item to which the word belongs.'), - ), - 'score' => array( - 'type' => 'float', - 'not null' => FALSE, - 'description' => t('The numeric score of the word, higher being more important.'), - ), - ), - 'indexes' => array( - 'sid_type' => array('sid', 'type'), - 'word' => array('word'), - ), - 'unique keys' => array( - 'word_sid_type' => array('word', 'sid', 'type'), - ), - ); - - $schema['search_total'] = array( - 'description' => t('Stores search totals for words.'), - 'fields' => array( - 'word' => array( - 'description' => t('Primary Key: Unique word in the search index.'), - 'type' => 'varchar', - 'length' => 50, - 'not null' => TRUE, - 'default' => '', - ), - 'count' => array( - 'description' => t("The count of the word in the index using Zipf's law to equalize the probability distribution."), - 'type' => 'float', - 'not null' => FALSE, - ), - ), - 'primary key' => array('word'), - ); - - $schema['search_node_links'] = array( - 'description' => t('Stores items (like nodes) that link to other nodes, used to improve search scores for nodes that are frequently linked to.'), - 'fields' => array( - 'sid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => t('The {search_dataset}.sid of the searchable item containing the link to the node.'), - ), - 'type' => array( - 'type' => 'varchar', - 'length' => 16, - 'not null' => TRUE, - 'default' => '', - 'description' => t('The {search_dataset}.type of the searchable item containing the link to the node.'), - ), - 'nid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'default' => 0, - 'description' => t('The {node}.nid that this item links to.'), - ), - 'caption' => array( - 'type' => 'text', - 'size' => 'big', - 'not null' => FALSE, - 'description' => t('The text used to link to the {node}.nid.'), - ), - ), - 'primary key' => array('sid', 'type', 'nid'), - 'indexes' => array( - 'nid' => array('nid'), - ), - ); - - return $schema; -} Index: modules/search/search.module =================================================================== RCS file: /cvs/drupal/drupal/modules/search/search.module,v retrieving revision 1.259 diff -u -p -r1.259 search.module --- modules/search/search.module 10 Jul 2008 02:13:02 -0000 1.259 +++ modules/search/search.module 13 Jul 2008 20:55:32 -0000 @@ -97,17 +97,8 @@ function search_help($path, $arg) { switch ($path) { case 'admin/help#search': $output = '

' . t('The search module adds the ability to search for content by keywords. Search is often the only practical way to find content on a large site, and is useful for finding both users and posts.') . '

'; - $output .= '

' . t('To provide keyword searching, the search engine maintains an index of words found in your site\'s content. To build and maintain this index, a correctly configured cron maintenance task is required. Indexing behavior can be adjusted using the search settings page; for example, the Number of items to index per cron run sets the maximum number of items indexed in each pass of a cron maintenance task. If necessary, reduce this number to prevent timeouts and memory errors when indexing.', array('@cron' => url('admin/reports/status'), '@searchsettings' => url('admin/settings/search'))) . '

'; $output .= '

' . t('For more information, see the online handbook entry for Search module.', array('@search' => 'http://drupal.org/handbook/modules/search/')) . '

'; return $output; - case 'admin/settings/search': - return '

' . t('The search engine maintains an index of words found in your site\'s content. To build and maintain this index, a correctly configured cron maintenance task is required. Indexing behavior can be adjusted using the settings below.', array('@cron' => url('admin/reports/status'))) . '

'; - case 'search#noresults': - return t(''); } } @@ -169,6 +160,7 @@ function search_block($op = 'list', $del * Implementation of hook_menu(). */ function search_menu() { + $items = array(); $items['search'] = array( 'title' => 'Search', 'page callback' => 'search_view', @@ -177,19 +169,13 @@ function search_menu() { ); $items['admin/settings/search'] = array( 'title' => 'Search settings', - 'description' => 'Configure relevance settings for search and other indexing options', + 'description' => 'Configure settings for search', 'page callback' => 'drupal_get_form', 'page arguments' => array('search_admin_settings'), 'access arguments' => array('administer search'), 'type' => MENU_NORMAL_ITEM, ); - $items['admin/settings/search/wipe'] = array( - 'title' => 'Clear index', - 'page callback' => 'drupal_get_form', - 'page arguments' => array('search_wipe_confirm'), - 'access arguments' => array('administer search'), - 'type' => MENU_CALLBACK, - ); + $items['admin/reports/search'] = array( 'title' => 'Top search phrases', 'description' => 'View most popular search phrases.', @@ -199,14 +185,14 @@ function search_menu() { 'file path' => drupal_get_path('module', 'dblog'), ); - foreach (module_implements('search') as $name) { + $types = search_types(); + foreach ($types as $name => $values) { $items['search/' . $name . '/%menu_tail'] = array( - 'title callback' => 'module_invoke', - 'title arguments' => array($name, 'search', 'name', TRUE), + 'title' => $values['title'], 'page callback' => 'search_view', 'page arguments' => array($name), 'access callback' => '_search_menu', - 'access arguments' => array($name), + 'access arguments' => array($values['access']), 'type' => MENU_LOCAL_TASK, 'parent' => 'search', ); @@ -214,183 +200,8 @@ function search_menu() { return $items; } -function _search_menu($name) { - return user_access('search content') && module_invoke($name, 'search', 'name'); -} - -/** - * Wipes a part of or the entire search index. - * - * @param $sid - * (optional) The SID of the item to wipe. If specified, $type must be passed - * too. - * @param $type - * (optional) The type of item to wipe. - */ -function search_wipe($sid = NULL, $type = NULL, $reindex = FALSE) { - if ($type == NULL && $sid == NULL) { - module_invoke_all('search', 'reset'); - } - else { - db_query("DELETE FROM {search_dataset} WHERE sid = %d AND type = '%s'", $sid, $type); - db_query("DELETE FROM {search_index} WHERE sid = %d AND type = '%s'", $sid, $type); - // Don't remove links if re-indexing. - if (!$reindex) { - db_query("DELETE FROM {search_node_links} WHERE sid = %d AND type = '%s'", $sid, $type); - } - } -} - -/** - * Marks a word as dirty (or retrieves the list of dirty words). This is used - * during indexing (cron). Words which are dirty have outdated total counts in - * the search_total table, and need to be recounted. - */ -function search_dirty($word = NULL) { - static $dirty = array(); - if ($word !== NULL) { - $dirty[$word] = TRUE; - } - else { - return $dirty; - } -} - -/** - * Implementation of hook_cron(). - * - * Fires hook_update_index() in all modules and cleans up dirty words (see - * search_dirty). - */ -function search_cron() { - // We register a shutdown function to ensure that search_total is always up - // to date. - register_shutdown_function('search_update_totals'); - - // Update word index - foreach (module_list() as $module) { - module_invoke($module, 'update_index'); - } -} - -/** - * This function is called on shutdown to ensure that search_total is always - * up to date (even if cron times out or otherwise fails). - */ -function search_update_totals() { - // Update word IDF (Inverse Document Frequency) counts for new/changed words - foreach (search_dirty() as $word => $dummy) { - // Get total count - $total = db_result(db_query("SELECT SUM(score) FROM {search_index} WHERE word = '%s'", $word)); - // Apply Zipf's law to equalize the probability distribution - $total = log10(1 + 1/(max(1, $total))); - db_query("UPDATE {search_total} SET count = %f WHERE word = '%s'", $total, $word); - if (!db_affected_rows()) { - db_query("INSERT INTO {search_total} (word, count) VALUES ('%s', %f)", $word, $total); - } - } - // Find words that were deleted from search_index, but are still in - // search_total. We use a LEFT JOIN between the two tables and keep only the - // rows which fail to join. - $result = db_query("SELECT t.word AS realword, i.word FROM {search_total} t LEFT JOIN {search_index} i ON t.word = i.word WHERE i.word IS NULL"); - while ($word = db_fetch_object($result)) { - db_query("DELETE FROM {search_total} WHERE word = '%s'", $word->realword); - } -} - -/** - * Simplifies a string according to indexing rules. - */ -function search_simplify($text) { - // Decode entities to UTF-8 - $text = decode_entities($text); - - // Lowercase - $text = drupal_strtolower($text); - - // Call an external processor for word handling. - search_invoke_preprocess($text); - - // Simple CJK handling - if (variable_get('overlap_cjk', TRUE)) { - $text = preg_replace_callback('/[' . PREG_CLASS_CJK . ']+/u', 'search_expand_cjk', $text); - } - - // To improve searching for numerical data such as dates, IP addresses - // or version numbers, we consider a group of numerical characters - // separated only by punctuation characters to be one piece. - // This also means that searching for e.g. '20/03/1984' also returns - // results with '20-03-1984' in them. - // Readable regexp: ([number]+)[punctuation]+(?=[number]) - $text = preg_replace('/([' . PREG_CLASS_NUMBERS . ']+)[' . PREG_CLASS_PUNCTUATION . ']+(?=[' . PREG_CLASS_NUMBERS . '])/u', '\1', $text); - - // The dot, underscore and dash are simply removed. This allows meaningful - // search behavior with acronyms and URLs. - $text = preg_replace('/[._-]+/', '', $text); - - // With the exception of the rules above, we consider all punctuation, - // marks, spacers, etc, to be a word boundary. - $text = preg_replace('/[' . PREG_CLASS_SEARCH_EXCLUDE . ']+/u', ' ', $text); - - return $text; -} - -/** - * Basic CJK tokenizer. Simply splits a string into consecutive, overlapping - * sequences of characters ('minimum_word_size' long). - */ -function search_expand_cjk($matches) { - $min = variable_get('minimum_word_size', 3); - $str = $matches[0]; - $l = drupal_strlen($str); - // Passthrough short words - if ($l <= $min) { - return ' ' . $str . ' '; - } - $tokens = ' '; - // FIFO queue of characters - $chars = array(); - // Begin loop - for ($i = 0; $i < $l; ++$i) { - // Grab next character - $current = drupal_substr($str, 0, 1); - $str = substr($str, strlen($current)); - $chars[] = $current; - if ($i >= $min - 1) { - $tokens .= implode('', $chars) . ' '; - array_shift($chars); - } - } - return $tokens; -} - -/** - * Splits a string into tokens for indexing. - */ -function search_index_split($text) { - static $last = NULL; - static $lastsplit = NULL; - - if ($last == $text) { - return $lastsplit; - } - // Process words - $text = search_simplify($text); - $words = explode(' ', $text); - array_walk($words, '_search_index_truncate'); - - // Save last keyword result - $last = $text; - $lastsplit = $words; - - return $words; -} - -/** - * Helper function for array_walk in search_index_split. - */ -function _search_index_truncate(&$text) { - $text = truncate_utf8($text, 50); +function _search_menu($access) { + return user_access('search content') && ($access ? user_access($access) : TRUE); } /** @@ -403,267 +214,7 @@ function search_invoke_preprocess(&$text } /** - * Update the full-text search index for a particular item. - * - * @param $sid - * A number identifying this particular item (e.g. node id). - * - * @param $type - * A string defining this type of item (e.g. 'node') - * - * @param $text - * The content of this item. Must be a piece of HTML text. - * - * @ingroup search - */ -function search_index($sid, $type, $text) { - $minimum_word_size = variable_get('minimum_word_size', 3); - - // Link matching - global $base_url; - $node_regexp = '@href=[\'"]?(?:' . preg_quote($base_url, '@') . '/|' . preg_quote(base_path(), '@') . ')(?:\?q=)?/?((?![a-z]+:)[^\'">]+)[\'">]@i'; - - // Multipliers for scores of words inside certain HTML tags. The weights are stored - // in a variable so that modules can overwrite the default weights. - // Note: 'a' must be included for link ranking to work. - $tags = variable_get('search_tag_weights', array( - 'h1' => 25, - 'h2' => 18, - 'h3' => 15, - 'h4' => 12, - 'h5' => 9, - 'h6' => 6, - 'u' => 3, - 'b' => 3, - 'i' => 3, - 'strong' => 3, - 'em' => 3, - 'a' => 10)); - - // Strip off all ignored tags to speed up processing, but insert space before/after - // them to keep word boundaries. - $text = str_replace(array('<', '>'), array(' <', '> '), $text); - $text = strip_tags($text, '<' . implode('><', array_keys($tags)) . '>'); - - // Split HTML tags from plain text. - $split = preg_split('/\s*<([^>]+?)>\s*/', $text, -1, PREG_SPLIT_DELIM_CAPTURE); - // Note: PHP ensures the array consists of alternating delimiters and literals - // and begins and ends with a literal (inserting $null as required). - - $tag = FALSE; // Odd/even counter. Tag or no tag. - $link = FALSE; // State variable for link analyser - $score = 1; // Starting score per word - $accum = ' '; // Accumulator for cleaned up data - $tagstack = array(); // Stack with open tags - $tagwords = 0; // Counter for consecutive words - $focus = 1; // Focus state - - $results = array(0 => array()); // Accumulator for words for index - - foreach ($split as $value) { - if ($tag) { - // Increase or decrease score per word based on tag - list($tagname) = explode(' ', $value, 2); - $tagname = drupal_strtolower($tagname); - // Closing or opening tag? - if ($tagname[0] == '/') { - $tagname = substr($tagname, 1); - // If we encounter unexpected tags, reset score to avoid incorrect boosting. - if (!count($tagstack) || $tagstack[0] != $tagname) { - $tagstack = array(); - $score = 1; - } - else { - // Remove from tag stack and decrement score - $score = max(1, $score - $tags[array_shift($tagstack)]); - } - if ($tagname == 'a') { - $link = FALSE; - } - } - else { - if (isset($tagstack[0]) && $tagstack[0] == $tagname) { - // None of the tags we look for make sense when nested identically. - // If they are, it's probably broken HTML. - $tagstack = array(); - $score = 1; - } - else { - // Add to open tag stack and increment score - array_unshift($tagstack, $tagname); - $score += $tags[$tagname]; - } - if ($tagname == 'a') { - // Check if link points to a node on this site - if (preg_match($node_regexp, $value, $match)) { - $path = drupal_get_normal_path($match[1]); - if (preg_match('!(?:node|book)/(?:view/)?([0-9]+)!i', $path, $match)) { - $linknid = $match[1]; - if ($linknid > 0) { - // Note: ignore links to uncachable nodes to avoid redirect bugs. - $node = db_fetch_object(db_query('SELECT n.title, n.nid, n.vid, r.format FROM {node} n INNER JOIN {node_revisions} r ON n.vid = r.vid WHERE n.nid = %d', $linknid)); - if (filter_format_allowcache($node->format)) { - $link = TRUE; - $linktitle = $node->title; - } - } - } - } - } - } - // A tag change occurred, reset counter. - $tagwords = 0; - } - else { - // Note: use of PREG_SPLIT_DELIM_CAPTURE above will introduce empty values - if ($value != '') { - if ($link) { - // Check to see if the node link text is its URL. If so, we use the target node title instead. - if (preg_match('!^https?://!i', $value)) { - $value = $linktitle; - } - } - $words = search_index_split($value); - foreach ($words as $word) { - // Add word to accumulator - $accum .= $word . ' '; - $num = is_numeric($word); - // Check wordlength - if ($num || drupal_strlen($word) >= $minimum_word_size) { - // Normalize numbers - if ($num) { - $word = (int)ltrim($word, '-0'); - } - - // Links score mainly for the target. - if ($link) { - if (!isset($results[$linknid])) { - $results[$linknid] = array(); - } - $results[$linknid][] = $word; - // Reduce score of the link caption in the source. - $focus *= 0.2; - } - // Fall-through - if (!isset($results[0][$word])) { - $results[0][$word] = 0; - } - $results[0][$word] += $score * $focus; - - // Focus is a decaying value in terms of the amount of unique words up to this point. - // From 100 words and more, it decays, to e.g. 0.5 at 500 words and 0.3 at 1000 words. - $focus = min(1, .01 + 3.5 / (2 + count($results[0]) * .015)); - } - $tagwords++; - // Too many words inside a single tag probably mean a tag was accidentally left open. - if (count($tagstack) && $tagwords >= 15) { - $tagstack = array(); - $score = 1; - } - } - } - } - $tag = !$tag; - } - - search_wipe($sid, $type, TRUE); - - // Insert cleaned up data into dataset - db_query("INSERT INTO {search_dataset} (sid, type, data, reindex) VALUES (%d, '%s', '%s', %d)", $sid, $type, $accum, 0); - - // Insert results into search index - foreach ($results[0] as $word => $score) { - // The database will collate similar words (accented and non-accented forms, etc.), - // and the score is additive, so first add and then insert. - db_query("UPDATE {search_index} SET score = score + %d WHERE word = '%s' AND sid = '%d' AND type = '%s'", $score, $word, $sid, $type); - if (!db_affected_rows()) { - db_query("INSERT INTO {search_index} (word, sid, type, score) VALUES ('%s', %d, '%s', %f)", $word, $sid, $type, $score); - } - search_dirty($word); - } - unset($results[0]); - - // Get all previous links from this item. - $result = db_query("SELECT nid, caption FROM {search_node_links} WHERE sid = %d AND type = '%s'", $sid, $type); - $links = array(); - while ($link = db_fetch_object($result)) { - $links[$link->nid] = $link->caption; - } - - // Now store links to nodes. - foreach ($results as $nid => $words) { - $caption = implode(' ', $words); - if (isset($links[$nid])) { - if ($links[$nid] != $caption) { - // Update the existing link and mark the node for reindexing. - db_query("UPDATE {search_node_links} SET caption = '%s' WHERE sid = %d AND type = '%s' AND nid = %d", $caption, $sid, $type, $nid); - search_touch_node($nid); - } - // Unset the link to mark it as processed. - unset($links[$nid]); - } - else { - // Insert the existing link and mark the node for reindexing. - db_query("INSERT INTO {search_node_links} (caption, sid, type, nid) VALUES ('%s', %d, '%s', %d)", $caption, $sid, $type, $nid); - search_touch_node($nid); - } - } - // Any left-over links in $links no longer exist. Delete them and mark the nodes for reindexing. - foreach ($links as $nid => $caption) { - db_query("DELETE FROM {search_node_links} WHERE sid = %d AND type = '%s' AND nid = %d", $sid, $type, $nid); - search_touch_node($nid); - } -} - -/** - * Change a node's changed timestamp to 'now' to force reindexing. - * - * @param $nid - * The nid of the node that needs reindexing. - */ -function search_touch_node($nid) { - db_query("UPDATE {search_dataset} SET reindex = %d WHERE sid = %d AND type = 'node'", time(), $nid); -} - -/** - * Implementation of hook_nodeapi(). - */ -function search_nodeapi(&$node, $op, $teaser = NULL, $page = NULL) { - switch ($op) { - // Transplant links to a node into the target node. - case 'update index': - $result = db_query("SELECT caption FROM {search_node_links} WHERE nid = %d", $node->nid); - $output = array(); - while ($link = db_fetch_object($result)) { - $output[] = $link->caption; - } - return '(' . implode(', ', $output) . ')'; - // Reindex the node when it is updated. The node is automatically indexed - // when it is added, simply by being added to the node table. - case 'update': - search_touch_node($node->nid); - break; - } -} - -/** - * Implementation of hook_comment(). - */ -function search_comment($a1, $op) { - switch ($op) { - // Reindex the node when comments are added or changed - case 'insert': - case 'update': - case 'delete': - case 'publish': - case 'unpublish': - search_touch_node(is_array($a1) ? $a1['nid'] : $a1->nid); - break; - } -} - -/** - * Extract a module-specific search option from a search query. e.g. 'type:book' + * Extract a type-specific search option from a search query. e.g. 'type:book' */ function search_query_extract($keys, $option) { if (preg_match('/(^| )' . $option . ':([^ ]*)( |$)/i', $keys, $matches)) { @@ -672,7 +223,7 @@ function search_query_extract($keys, $op } /** - * Return a query with the given module-specific search option inserted in. + * Return a query with the given type-specific search option inserted in. * e.g. 'type:book'. */ function search_query_insert($keys, $option, $value = '') { @@ -686,24 +237,12 @@ function search_query_insert($keys, $opt } /** - * Parse a search query into SQL conditions. - * - * We build two queries that matches the dataset bodies. @See do_search for - * more about these. + * Parse a search query into an array of conditions. * * @param $text * The search keys. * @return - * A list of six elements. - * * A series of statements AND'd together which will be used to provide all - * possible matches. - * * Arguments for this query part. - * * A series of exact word matches OR'd together. - * * Arguments for this query part. - * * A bool indicating whether this is a simple query or not. Negative - * terms, presence of both AND / OR make this FALSE. - * * A bool indicating the presence of a lowercase or. Maybe the user - * wanted to use OR. + * An array of keys and conditions. */ function search_parse_query($text) { $keys = array('positive' => array(), 'negative' => array()); @@ -728,7 +267,7 @@ function search_parse_query($text) { $simple = FALSE; } // Simplify keyword according to indexing rules and external preprocessors - $words = search_simplify($match[2]); + $words = search_invoke_handler('simplify', $match[2]); // Re-explode in case simplification added more words, except when matching a phrase $words = $phrase ? array($words) : preg_split('/ /', $words, -1, PREG_SPLIT_NO_EMPTY); // Negative matches @@ -768,196 +307,7 @@ function search_parse_query($text) { } $or = FALSE; } - - // Convert keywords into SQL statements. - $query = array(); - $query2 = array(); - $arguments = array(); - $arguments2 = array(); - $matches = 0; - $simple_and = FALSE; - $simple_or = FALSE; - // Positive matches - foreach ($keys['positive'] as $key) { - // Group of ORed terms - if (is_array($key) && count($key)) { - $simple_or = TRUE; - $queryor = array(); - $any = FALSE; - foreach ($key as $or) { - list($q, $num_new_scores) = _search_parse_query($or, $arguments2); - $any |= $num_new_scores; - if ($q) { - $queryor[] = $q; - $arguments[] = $or; - } - } - if (count($queryor)) { - $query[] = '(' . implode(' OR ', $queryor) . ')'; - // A group of OR keywords only needs to match once - $matches += ($any > 0); - } - } - // Single ANDed term - else { - $simple_and = TRUE; - list($q, $num_new_scores, $num_valid_words) = _search_parse_query($key, $arguments2); - if ($q) { - $query[] = $q; - $arguments[] = $key; - if (!$num_valid_words) { - $simple = FALSE; - } - // Each AND keyword needs to match at least once - $matches += $num_new_scores; - } - } - } - if ($simple_and && $simple_or) { - $simple = FALSE; - } - // Negative matches - foreach ($keys['negative'] as $key) { - list($q) = _search_parse_query($key, $arguments2, TRUE); - if ($q) { - $query[] = $q; - $arguments[] = $key; - $simple = FALSE; - } - } - $query = implode(' AND ', $query); - - // Build word-index conditions for the first pass - $query2 = substr(str_repeat("i.word = '%s' OR ", count($arguments2)), 0, -4); - - return array($query, $arguments, $query2, $arguments2, $matches, $simple, $warning); -} - -/** - * Helper function for search_parse_query(); - */ -function _search_parse_query(&$word, &$scores, $not = FALSE) { - $num_new_scores = 0; - $num_valid_words = 0; - // Determine the scorewords of this word/phrase - if (!$not) { - $split = explode(' ', $word); - foreach ($split as $s) { - $num = is_numeric($s); - if ($num || drupal_strlen($s) >= variable_get('minimum_word_size', 3)) { - $s = $num ? ((int)ltrim($s, '-0')) : $s; - if (!isset($scores[$s])) { - $scores[$s] = $s; - $num_new_scores++; - } - $num_valid_words++; - } - } - } - // Return matching snippet and number of added words - return array("d.data " . ($not ? 'NOT ' : '') . "LIKE '%% %s %%'", $num_new_scores, $num_valid_words); -} - -/** - * Do a query on the full-text search index for a word or words. - * - * This function is normally only called by each module that support the - * indexed search (and thus, implements hook_update_index()). - * - * Results are retrieved in two logical passes. However, the two passes are - * joined together into a single query. And in the case of most simple - * queries the second pass is not even used. - * - * The first pass selects a set of all possible matches, which has the benefit - * of also providing the exact result set for simple "AND" or "OR" searches. - * - * The second portion of the query further refines this set by verifying - * advanced text conditions (such negative or phrase matches) - * - * @param $keywords - * A search string as entered by the user. - * - * @param $type - * A string identifying the calling module. - * - * @param $join1 - * (optional) Inserted into the JOIN part of the first SQL query. - * For example "INNER JOIN {node} n ON n.nid = i.sid". - * - * @param $where1 - * (optional) Inserted into the WHERE part of the first SQL query. - * For example "(n.status > %d)". - * - * @param $arguments1 - * (optional) Extra SQL arguments belonging to the first query. - * - * @param $columns2 - * (optional) Inserted into the SELECT pat of the second query. Must contain - * a column selected as 'score'. - * defaults to 'i.relevance AS score' - * - * @param $join2 - * (optional) Inserted into the JOIN par of the second SQL query. - * For example "INNER JOIN {node_comment_statistics} n ON n.nid = i.sid" - * - * @param $arguments2 - * (optional) Extra SQL arguments belonging to the second query parameter. - * - * @param $sort_parameters - * (optional) SQL arguments for sorting the final results. - * Default: 'ORDER BY score DESC' - * - * @return - * An array of SIDs for the search results. - * - * @ingroup search - */ -function do_search($keywords, $type, $join1 = '', $where1 = '1', $arguments1 = array(), $columns2 = 'i.relevance AS score', $join2 = '', $arguments2 = array(), $sort_parameters = 'ORDER BY score DESC') { - $query = search_parse_query($keywords); - - if ($query[2] == '') { - form_set_error('keys', t('You must include at least one positive keyword with @count characters or more.', array('@count' => variable_get('minimum_word_size', 3)))); - } - if ($query[6]) { - if ($query[6] == 'or') { - drupal_set_message(t('Search for either of the two terms with uppercase OR. For example, cats OR dogs.')); - } - } - if ($query === NULL || $query[0] == '' || $query[2] == '') { - return array(); - } - - // Build query for keyword normalization. - $conditions = "$where1 AND ($query[2]) AND i.type = '%s'"; - $arguments1 = array_merge($arguments1, $query[3], array($type)); - $join = "INNER JOIN {search_total} t ON i.word = t.word $join1"; - if (!$query[5]) { - $conditions .= " AND ($query[0])"; - $arguments1 = array_merge($arguments1, $query[1]); - $join .= " INNER JOIN {search_dataset} d ON i.sid = d.sid AND i.type = d.type"; - } - - // Calculate maximum keyword relevance, to normalize it. - $select = "SELECT SUM(i.score * t.count) AS score FROM {search_index} i $join WHERE $conditions GROUP BY i.type, i.sid HAVING COUNT(*) >= %d ORDER BY score DESC"; - $arguments = array_merge($arguments1, array($query[4])); - $normalize = db_result(db_query_range($select, $arguments, 0, 1)); - if (!$normalize) { - return array(); - } - $columns2 = str_replace('i.relevance', '(' . (1.0 / $normalize) . ' * SUM(i.score * t.count))', $columns2); - - // Build query to retrieve results. - $select = "SELECT i.type, i.sid, $columns2 FROM {search_index} i $join $join2 WHERE $conditions GROUP BY i.type, i.sid HAVING COUNT(*) >= %d"; - $count_select = "SELECT COUNT(*) FROM ($select) n1"; - $arguments = array_merge($arguments2, $arguments1, array($query[4])); - - // Do actual search query - $result = pager_query("$select $sort_parameters", 10, 0, $count_select, $arguments); - $results = array(); - while ($item = db_fetch_object($result)) { - $results[] = $item; - } - return $results; + return array($keys, $or, $warning, $simple); } /** @@ -1034,7 +384,7 @@ function search_form(&$form_state, $acti '#action' => $action, '#attributes' => array('class' => 'search-form'), ); - $form['module'] = array('#type' => 'value', '#value' => $type); + $form['search_type'] = array('#type' => 'value', '#value' => $type); $form['basic'] = array('#type' => 'item', '#title' => $prompt); $form['basic']['inline'] = array('#prefix' => '
', '#suffix' => '
'); $form['basic']['inline']['keys'] = array( @@ -1139,19 +489,8 @@ function template_preprocess_search_bloc * Perform a standard search on the given keys, and return the formatted results. */ function search_data($keys = NULL, $type = 'node') { - if (isset($keys)) { - if (module_hook($type, 'search')) { - $results = module_invoke($type, 'search', 'search', $keys); - if (isset($results) && is_array($results) && count($results)) { - if (module_hook($type, 'search_page')) { - return module_invoke($type, 'search_page', $results); - } - else { - return theme('search_results', $results, $type); - } - } - } + return search_invoke_handler('data', $type, $keys); } } @@ -1268,6 +607,53 @@ function search_excerpt($keys, $text) { } /** + * Return available search handlers. + */ +function search_handlers($refresh = FALSE) { + static $handlers; + if (empty($handlers)) { + $handlers = module_invoke_all('search'); + } + return $handlers; +} + +/** + * Return the search types supported by the active search handler. + */ +function search_types($names = FALSE, $refresh = FALSE) { + static $types; + if (empty($types) || $refresh) { + $handlers = search_handlers(); + $active_handler = search_active_handler(); + $types = isset($handlers[$active_handler]) ? $handlers[$active_handler]['types'] : array(); + } + return $names ? array_keys($types) : $types; +} + +/** + * Return the active search handler. + */ +function search_active_handler() { + return variable_get('search_handler', _search_default_handler()); +} + +/** + * Return the default search handler. + */ +function _search_default_handler() { + return module_exists('sql_search') ? 'sql_search' : NULL; +} + +/** + * Call a hook in the active search handler. + */ +function search_invoke_handler($hook) { + $args = func_get_args(); + unset($args[0]); + return call_user_func_array(search_active_handler() . '_' . $hook, $args); +} + +/** * @} End of "defgroup search". */ @@ -1289,3 +675,4 @@ function search_forms() { ); return $forms; } + Index: modules/search/search.pages.inc =================================================================== RCS file: /cvs/drupal/drupal/modules/search/search.pages.inc,v retrieving revision 1.5 diff -u -p -r1.5 search.pages.inc --- modules/search/search.pages.inc 14 Apr 2008 17:48:41 -0000 1.5 +++ modules/search/search.pages.inc 13 Jul 2008 20:55:32 -0000 @@ -124,7 +124,7 @@ function search_form_submit($form, &$for // Fall through to the drupal_goto() call. } - $type = $form_state['values']['module'] ? $form_state['values']['module'] : 'node'; + $type = $form_state['values']['search_type'] ? $form_state['values']['search_type'] : 'node'; $form_state['redirect'] = 'search/' . $type . '/' . $keys; return; } Index: modules/system/system.module =================================================================== RCS file: /cvs/drupal/drupal/modules/system/system.module,v retrieving revision 1.606 diff -u -p -r1.606 system.module --- modules/system/system.module 8 Jul 2008 01:08:15 -0000 1.606 +++ modules/system/system.module 13 Jul 2008 20:55:34 -0000 @@ -1178,6 +1178,9 @@ function system_initialize_theme_blocks( * The form structure. */ function system_settings_form($form) { + $form['buttons'] = array( + '#weight' => 5, + ); $form['buttons']['submit'] = array('#type' => 'submit', '#value' => t('Save configuration') ); $form['buttons']['reset'] = array('#type' => 'submit', '#value' => t('Reset to defaults') ); Index: modules/user/user.info =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.info,v retrieving revision 1.6 diff -u -p -r1.6 user.info --- modules/user/user.info 6 May 2008 12:18:54 -0000 1.6 +++ modules/user/user.info 13 Jul 2008 20:55:34 -0000 @@ -7,3 +7,4 @@ core = 7.x files[] = user.module files[] = user.admin.inc files[] = user.pages.inc +files[] = user.sql_search.inc Index: modules/user/user.module =================================================================== RCS file: /cvs/drupal/drupal/modules/user/user.module,v retrieving revision 1.911 diff -u -p -r1.911 user.module --- modules/user/user.module 27 Jun 2008 07:25:11 -0000 1.911 +++ modules/user/user.module 13 Jul 2008 20:55:36 -0000 @@ -591,38 +591,6 @@ function user_file_download($file) { } /** - * Implementation of hook_search(). - */ -function user_search($op = 'search', $keys = NULL, $skip_access_check = FALSE) { - switch ($op) { - case 'name': - if ($skip_access_check || user_access('access user profiles')) { - return t('Users'); - } - case 'search': - if (user_access('access user profiles')) { - $find = array(); - // Replace wildcards with MySQL/PostgreSQL wildcards. - $keys = preg_replace('!\*+!', '%', $keys); - if (user_access('administer users')) { - // Administrators can also search in the otherwise private email field. - $result = pager_query("SELECT name, uid, mail FROM {users} WHERE LOWER(name) LIKE LOWER('%%%s%%') OR LOWER(mail) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys, $keys); - while ($account = db_fetch_object($result)) { - $find[] = array('title' => $account->name . ' (' . $account->mail . ')', 'link' => url('user/' . $account->uid, array('absolute' => TRUE))); - } - } - else { - $result = pager_query("SELECT name, uid FROM {users} WHERE LOWER(name) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys); - while ($account = db_fetch_object($result)) { - $find[] = array('title' => $account->name, 'link' => url('user/' . $account->uid, array('absolute' => TRUE))); - } - } - return $find; - } - } -} - -/** * Implementation of hook_elements(). */ function user_elements() { @@ -1092,6 +1060,9 @@ function user_menu() { function user_init() { drupal_add_css(drupal_get_path('module', 'user') . '/user.css', 'module'); + if (module_exists('sql_search')) { + module_load_include('inc', 'user', 'user.sql_search'); + } } function user_uid_optional_load($arg) { Index: modules/user/user.sql_search.inc =================================================================== RCS file: modules/user/user.sql_search.inc diff -N modules/user/user.sql_search.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ modules/user/user.sql_search.inc 13 Jul 2008 20:55:36 -0000 @@ -0,0 +1,35 @@ + t('Users'), + 'access' => 'access user profiles', + ); + case 'search': + if (user_access('access user profiles')) { + $find = array(); + // Replace wildcards with MySQL/PostgreSQL wildcards. + $keys = preg_replace('!\*+!', '%', $keys); + if (user_access('administer users')) { + // Administrators can also search in the otherwise private email field. + $result = pager_query("SELECT name, uid, mail FROM {users} WHERE LOWER(name) LIKE LOWER('%%%s%%') OR LOWER(mail) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys, $keys); + while ($account = db_fetch_object($result)) { + $find[] = array('title' => $account->name . ' (' . $account->mail . ')', 'link' => url('user/' . $account->uid, array('absolute' => TRUE))); + } + } + else { + $result = pager_query("SELECT name, uid FROM {users} WHERE LOWER(name) LIKE LOWER('%%%s%%')", 15, 0, NULL, $keys); + while ($account = db_fetch_object($result)) { + $find[] = array('title' => $account->name, 'link' => url('user/' . $account->uid, array('absolute' => TRUE))); + } + } + return $find; + } + } +}