'. t('Sphinx search module provides a fast and scalable alternative to Drupal core search for content. Sphinx search implementation is based on XMLPipe source type and support for main+delta index scheme.') .'

'; return $output; case 'admin/settings/sphinxsearch': return '

'. t('You can adjust the settings below to tweak the Sphinx behaviour.') .'

'; case 'sphinxsearch#noresults': return t('Suggestions:'); } } /** * Implementation of hook_perm(). */ function sphinxsearch_perm() { return array('use sphinxsearch', 'administer sphinxsearch'); } /** * Implementation of hook_menu(). */ function sphinxsearch_menu($may_cache) { $items = array(); $module_path = drupal_get_path('module', 'sphinxsearch'); require_once($module_path .'/sphinxsearch.common.inc'); if ($may_cache) { $admin_access = user_access('administer sphinxsearch'); $usage_access = user_access('use sphinxsearch'); $items[] = array( 'path' => 'admin/settings/sphinxsearch', 'title' => t('Sphinx search'), 'callback' => 'drupal_get_form', 'callback arguments' => 'sphinxsearch_settings', 'access' => $admin_access, ); $items[] = array( 'path' => 'admin/settings/sphinxsearch/settings', 'title' => t('Settings'), 'description' => t('Administer Sphinx search module settings'), 'access' => $admin_access, 'weight' => -10, 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items[] = array( 'path' => 'admin/settings/sphinxsearch/check-connection', 'title' => t('Check connection'), 'description' => t('Check connection to Sphinx searchd daemon'), 'callback' => 'sphinxsearch_check_connection_page', 'access' => $admin_access, 'weight' => 10, 'type' => MENU_LOCAL_TASK, ); $items[] = array( 'path' => sphinxsearch_get_search_path(), 'title' => t('Search'), 'callback' => 'sphinxsearch_search_page', 'access' => $usage_access, 'type' => MENU_SUGGESTED_ITEM, ); } else { if (arg(0) == 'admin' && arg(1) == 'settings' && arg(2) == 'sphinxsearch') { require_once($module_path .'/sphinxsearch.admin.inc'); } else if (sphinxsearch_is_search_path()) { require_once($module_path .'/sphinxsearch.pages.inc'); } /** * FIXME * We need our own CSS in all pages because of * tagadelic and similar blocks. */ drupal_add_css($module_path .'/sphinxsearch.css'); } return $items; } /** * Implementation of hook_block(). */ function sphinxsearch_block($op = 'list', $delta = 0, $edit = array()) { if ($op == 'list') { $blocks = array( 'searchbox' => array('info' => t('Sphinx search box')), ); if (module_exists('taxonomy')) { foreach (sphinxsearch_get_enabled_vocabularies() as $vid => $vocabulary) { $blocks['tags'. $vid] = array('info' => t('Sphinx tags in @vocabulary', array('@vocabulary' => $vocabulary->name))); } } return $blocks; } else if ($op == 'configure') { if ($delta != 'searchbox') { $vid = (int)str_replace('tags', '', $delta); $vocabularies = sphinxsearch_get_enabled_vocabularies(); if (isset($vocabularies[$vid])) { $options = array( 'weight,asc' => t('by weight, ascending'), 'weight,desc' => t('by weight, descending'), 'title,asc' => t('by title, ascending'), 'title,desc' => t('by title, descending'), 'random,none' => t('random') ); $form['sortmode'] = array( '#type' => 'radios', '#title' => t('Tagadelic sort order'), '#options' => $options, '#default_value' => variable_get('sphinxsearch_tagadelic_block_sortmode_'. $vid, 'title,asc'), '#description' => t('Determines the sort order of the tags in the cloud.'), ); $form['tags'] = array( '#type' => 'select', '#title' => t('Tags to show'), '#options' => drupal_map_assoc(array(5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 60, 70, 80, 90, 100)), '#default_value' => variable_get('sphinxsearch_tagadelic_block_tags_'. $vid, 20), '#description' => t('The number of tags to show in this block.'), ); $form['levels'] = array( '#type' => 'select', '#title' => t('Number of levels'), '#options' => drupal_map_assoc(array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16)), '#default_value' => variable_get('sphinxsearch_tagadelic_block_levels_'. $vid, 10), '#description' => t('The number of levels between the least popular tags and the most popular ones. Different levels will be assigned a different class to be themed (see sphinxsearch.css).'), ); return $form; } } } else if ($op == 'save') { if ($delta != 'searchbox') { $vid = (int)str_replace('tags', '', $delta); $vocabularies = sphinxsearch_get_enabled_vocabularies(); if (isset($vocabularies[$vid])) { variable_set('sphinxsearch_tagadelic_block_sortmode_'. $vid, $edit['sortmode']); variable_set('sphinxsearch_tagadelic_block_tags_'. $vid, $edit['tags']); variable_set('sphinxsearch_tagadelic_block_levels_'. $vid, $edit['levels']); } } } else if ($op == 'view' && user_access('use sphinxsearch')) { if ($delta == 'searchbox') { // Hide block if current page is search. if (!sphinxsearch_is_search_path()) { return array( 'subject' => t('Search'), 'content' => drupal_get_form('sphinxsearch_search_box'), ); } } else { $vid = (int)str_replace('tags', '', $delta); $vocabularies = sphinxsearch_get_enabled_vocabularies(); if (isset($vocabularies[$vid])) { $search_results = sphinxsearch_execute_query(array_merge(sphinxsearch_parse_request(), array( 'results_per_page' => variable_get('sphinxsearch_tagadelic_block_tags_'. $vid, 20), 'group_by' => 'terms'. $vid, ))); $tags = sphinxsearch_tagadelic_build_tags( $search_results, $vid, variable_get('sphinxsearch_tagadelic_block_levels_'. $vid, 10), variable_get('sphinxsearch_tagadelic_block_sortmode_'. $vid, 'title,asc') ); if (!empty($tags)) { return array( 'subject' => t('Tags in @vocabulary', array('@vocabulary' => $vocabularies[$vid]->name)), 'content' => theme('sphinxsearch_tagadelic_block', $tags), ); } } } } } /** * Build tagadelic items for the given Sphinx search results. * * @param array $search_results * Search results structure. * @param int $vid * Vocabulary ID. * @param int $levels * Amount of tag-sizes. * @param string $sort_mode * Sort order options. * * @return array */ function sphinxsearch_tagadelic_build_tags($search_results, $vid, $levels = 10, $sort_mode = 'title,asc') { $tags = array(); if ($search_results['total_available'] > 0) { $terms = sphinxsearch_taxonomy_get_terms($vid, array_keys($search_results['groups'])); // Find minimum and maximum log-count. Algorithm based on tagadelic.module. $min = 1e9; $max = -1e9; foreach ($search_results['groups'] as $tid => $group_info) { if (isset($terms[$tid])) { $tag = $terms[$tid]; $tag->tagadelic_count = log($group_info['count']); $min = min($min, $tag->tagadelic_count); $max = max($max, $tag->tagadelic_count); $tags[$tid] = $tag; } } unset($terms); // Note: we need to ensure the range is slightly too large to make sure even // the largest element is rounded down. $range = max(.01, $max - $min) * 1.0001; foreach ($tags as $tid => $tag) { $tags[$tid]->tagadelic_weight = 1 + floor($levels * ($tag->tagadelic_count - $min) / $range); } // Sort tags. list($sort_by, $sort_order) = explode(',', $sort_mode); switch ($sort_by) { case 'title': usort($tags, '_sphinxsearch_tagadelic_sort_by_title'); break; case 'weight': usort($tags, '_sphinxsearch_tagadelic_sort_by_weight'); break; case 'random': shuffle($tags); break; } if ($sort_order == 'desc') { $tags = array_reverse($tags); } } return $tags; } /** * Callback for usort, sort by count. */ function _sphinxsearch_tagadelic_sort_by_title($a, $b) { return strnatcasecmp($a->name, $b->name); } /** * Callback for usort, sort by weight. */ function _sphinxsearch_tagadelic_sort_by_weight($a, $b) { return $a->tagadelic_weight > $b->tagadelic_weight; } /** * Format tagadelic block for the given tags. * * @param array $tags * * @ingroup themeable */ function theme_sphinxsearch_tagadelic_block($tags) { $output = ''; foreach ($tags as $tag) { $output .= l($tag->name, taxonomy_term_path($tag), array('class' => 'tagadelic-level'. $tag->tagadelic_weight, 'rel' => 'tag')) ."\n"; } if (!empty($output)) { $output = '
'. $output .'
'; } return $output; } /** * Render a search box form. */ function sphinxsearch_search_box() { $form = array(); // Build basic search box form. $form['inline'] = array('#prefix' => '
', '#suffix' => '
'); $form['inline']['keys'] = array( '#type' => 'textfield', '#size' => 30, '#default_value' => '', '#attributes' => array('title' => t('Enter the terms you wish to search for.')), ); $form['inline']['submit'] = array('#type' => 'submit', '#value' => t('Search')); $form['#action'] = url(sphinxsearch_get_search_path()); return $form; } /** * Process a search box form submission. */ function sphinxsearch_search_box_submit($form_id, $form_values) { $query = array(); $keys = preg_replace('#\s+#', ' ', trim($form_values['keys'])); if (!empty($keys)) { $query['keys'] = $keys; } // Transform POST into a GET request. sphinxsearch_goto_search($query); } /** * Implementation of hook_nodeapi(). */ function sphinxsearch_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { if ($op == 'delete') { // Update Sphinx documents when node is deleted. $sphinxsearch_enabled_node_types = sphinxsearch_get_enabled_node_types(); if (empty($sphinxsearch_enabled_node_types) || in_array($node->type, $sphinxsearch_enabled_node_types)) { sphinxsearch_delete_node_from_index($node->nid); } } } /** * Delete a node from Sphinx indexes. * * Sphinx document deletions are updated in realtime using UpdateAttributes() * API to set the is_deleted attribute of the document to 1. * These Sphinx updates are sent to all indexes behind a distributed index. * To completely remove all deleted nodes from Sphinx indexes, it is * necessary to rebuild main indexes from time to time. * * @param int $nid * Node Identifier. */ function sphinxsearch_delete_node_from_index($nid) { $sphinxsearch_query_index = variable_get('sphinxsearch_query_index', ''); $sphinxsearch_docid_offset = (int)variable_get('sphinxsearch_docid_offset', 0); $sphinxsearch = sphinxsearch_get_client(); $count = $sphinxsearch->UpdateAttributes($sphinxsearch_query_index, array('is_deleted'), array( ($nid + $sphinxsearch_docid_offset) => array(1) )); // Note: count should be number of updated documents, -1 if error. if ($count <= 0) { watchdog('sphinxsearch', t('Node @nid could not be deleted from Sphinx index. Last Sphinx Error: !message', array( '@nid' => $nid, '!message' => $sphinxsearch->GetLastError() )), WATCHDOG_WARNING); } } /** * Encode terms hash into a comma separated list. * * @param array $terms * Hash of terms. * @param string $separator * Terms separator. Defaults to ', '. * @return string * Comma separated list of terms. */ function sphinxsearch_taxonomy_encode_typed_terms($terms, $separator = ', ') { $typed_terms = array(); foreach ($terms as $tid => $term) { // Commas and quotes in terms are special cases, so encode 'em. if (strpos($term->name, ',') !== FALSE || strpos($term->name, '"') !== FALSE) { $term->name = '"'. str_replace('"', '""', $term->name) .'"'; } $typed_terms[] = $term->name; } return implode($separator, $typed_terms); } /** * Decode a comma separated list of strings into hash of terms. * @see taxonomy_node_save() * * @param int $vid * Vocabulary identifier related to terms. * @param string $typed_terms * Comma separated list of terms. * @return array * Hash of terms. Note that a string of not found typed terms is stored as tid -1. */ function sphinxsearch_taxonomy_decode_typed_terms($vid, $typed_terms) { $terms = array(); $regexp = '%(?:^|,\ *)("(?>[^"]*)(?>""[^"]* )*"|(?: [^",]*))%x'; preg_match_all($regexp, $typed_terms, $matches); $typed_terms = array_unique($matches[1]); foreach ($typed_terms as $typed_term) { $typed_term = trim(str_replace('""', '"', preg_replace('#^"(.*)"$#', '\1', $typed_term))); if (!empty($typed_term)) { $possibilities = taxonomy_get_term_by_name($typed_term); $term = NULL; foreach ($possibilities as $possibility) { if ($possibility->vid == $vid) { $term = $possibility; } } if ($term) { $terms[$term->tid] = $term; } else { // Store typed terms that we haven't found in a particular item with tid = -1, // so caller has the chance to notify the user about it. if (!isset($terms[-1])) { $terms[-1] = array(); } $terms[-1][] = $typed_term; } } } if (isset($terms[-1])) { $terms[-1] = implode(', ', $terms[-1]); } return $terms; } /** * Parse a comma separated list of tids. * * @param int $vid * Vocabulary identifier related to terms. * @param array $tids * List of tids. * @return array * Hash of terms. Note that a string of not found tids is stored as tid -1. */ function sphinxsearch_taxonomy_get_terms($vid, $tids) { $terms = array(); if (!empty($tids)) { $tids_count = count($tids); $query_args = array_merge(array($vid), $tids); $query_sql = 'SELECT t.* FROM {term_data} t WHERE t.vid = %d AND t.tid '; if ($tids_count == 1) { $query_sql .= '= %d'; } else { $placeholders = implode(',', array_fill(0, count($tids), '%d')); $query_sql .= 'IN ('. $placeholders .')'; } $result = db_query(db_rewrite_sql($query_sql, 't', 'tid'), $query_args); while ($term = db_fetch_object($result)) { $terms[$term->tid] = $term; } $diff = array_diff($tids, array_keys($terms)); if (!empty($diff)) { $terms[-1] = implode(', ', $diff); } } return $terms; } /** * Obtain PHP memory_limit. * * Requirements: PHP needs to be compiled with --enable-memory-limit. * @see http://www.php.net/manual/en/faq.using.php#faq.using.shorthandbytes * * @return int * Memory limit in bytes, -1 if error. */ function sphinxsearch_get_memory_limit() { if (!function_exists('memory_get_usage')) { return -1; } $memory_limit = trim(@ini_get('memory_limit')); if (is_numeric($memory_limit)) { $memory_limit = (int)$memory_limit; } else { if (!preg_match('#([0-9]+)(K|M|G)#', strtoupper($memory_limit), $matches)) { return -1; } $memory_limit = (int)$matches[1]; switch ($matches[2]) { case 'G': $memory_limit *= 1024; case 'M': $memory_limit *= 1024; case 'K': $memory_limit *= 1024; } } return $memory_limit; } /** * Obtain list of node types enabled to be indexed. * * @return array * list of node types. */ function sphinxsearch_get_enabled_node_types() { static $node_types; if (!isset($node_types)) { $node_types = array(); foreach (node_get_types() as $node_type) { if (variable_get('sphinxsearch_include_node_type_'. $node_type->type, 0)) { $node_types[] = $node_type->type; } } } return $node_types; } /** * Build SQL condition for filtering nodes by enabled node types. * * @param string $table_alias * Table alias. Empty by default. Tipical alias for node table is 'n'. * @return string * SQL condition. */ function sphinxsearch_get_enabled_node_types_condition($table_alias = '') { static $condition; if (!isset($condition)) { $condition = ''; $sphinxsearch_enabled_node_types = sphinxsearch_get_enabled_node_types(); if (!empty($sphinxsearch_enabled_node_types)) { $sphinxsearch_enabled_node_types_count = count($sphinxsearch_enabled_node_types); if ($sphinxsearch_enabled_node_types_count == 1) { $condition .= '= \'%s\''; } else { $types = array(); foreach ($sphinxsearch_enabled_node_types as $type) { $types[] = "'". db_escape_string($type) ."'"; } $condition .= 'IN ('. implode(', ', $types) .')'; } } } if (empty($condition)) { return ''; } return (!empty($table_alias) ? $table_alias .'.' : '') .'type '. $condition; } /** * Obtain list of enabled vocabularies. * * Each vocabulary will be indexed on a separate multi-valued field. * * @return array * list of enabled vocabularies. */ function sphinxsearch_get_enabled_vocabularies() { static $vocabularies; if (!isset($vocabularies)) { $vocabularies = array(); if (module_exists('taxonomy')) { foreach(taxonomy_get_vocabularies() as $vocabulary) { if (variable_get('sphinxsearch_include_vocabulary_'. $vocabulary->vid, 0)) { $vocabularies[(int)$vocabulary->vid] = $vocabulary; } } } } return $vocabularies; } /** * Initialize content type -vs- unique numeric ids table. * * This array is used to convert node types as string to * numeric identifiers, so this data can be stored in * Sphinx indexes as simple uint attributes. */ function sphinxsearch_reset_node_type_ids() { variable_set('sphinxsearch_node_type_ids', array('')); } /** * Get a unique numeric ID for the given node type. */ function sphinxsearch_get_node_type_id($type) { static $node_type_ids; if (!isset($node_type_ids)) { $node_type_ids = variable_get('sphinxsearch_node_type_ids', array('')); } if (!in_array($type, $node_type_ids)) { $node_type_ids[] = $type; variable_set('sphinxsearch_node_type_ids', $node_type_ids); } return (int)array_search($type, $node_type_ids); } /** * Obtain the text representation of a node. * All HTML is removed. * * @param object reference $node * Node reference to extract text from. * @return string * Text representation of the node. */ function sphinxsearch_get_node_text(&$node) { // Build the node body. $node = node_build_content($node, FALSE, FALSE); $node->body = drupal_render($node->content); // Allow modules to modify the fully-built node. node_invoke_nodeapi($node, 'alter'); $text = check_plain($node->title) ."\n". $node->body; // Fetch extra data normally not visible $extra = node_invoke_nodeapi($node, 'update index'); foreach ($extra as $t) { $text .= $t; } unset($extra, $t); // Strip control characters that aren't valid in xml. // See http://www.w3.org/International/questions/qa-controls $text = preg_replace('#[\\x00-\\x08\\x0B\\x0C\\x0E-\\x1F]#S', ' ', $text); // Strip off all tags, but insert space before/after them to keep word boundaries. $text = str_replace(array('<', '>', '[', ']'), array(' <', '> ', ' ', ' '), $text); $text = preg_replace('#<(script|style)[^>]*>.*#', ' ', $text); $text = strip_tags($text); // Reduce size a little removing redudant spaces and line breaks. $text = preg_replace("# +#", ' ', $text); $text = preg_replace("#(\s*)\n+#", "\n", $text); return $text; }