'. 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:
- Make sure all words are spelled correctly.
- Try different or more general keywords.
- Consider using less restrictive filters.
');
}
}
/**
* 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)[^>]*>.*\1>#', ' ', $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;
}