'Apache Solr', 'description' => 'Administer Apache Solr.', 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_settings'), 'access callback' => 'user_access', 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', ); $items['admin/settings/apachesolr/settings'] = array( 'title' => 'Settings', 'weight' => -10, 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['admin/settings/apachesolr/enabled-filters'] = array( 'title' => 'Enabled filters', 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_enabled_facets_form'), 'weight' => -7, 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_LOCAL_TASK, ); $items['admin/settings/apachesolr/index'] = array( 'title' => 'Search index', 'page callback' => 'apachesolr_index_page', 'access arguments' => array('administer search'), 'weight' => -8, 'file' => 'apachesolr.admin.inc', 'type' => MENU_LOCAL_TASK, ); $items['admin/settings/apachesolr/index/clear/confirm'] = array( 'title' => 'Confirm the re-indexing of all content', 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_clear_index_confirm'), 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_CALLBACK, ); $items['admin/settings/apachesolr/index/delete/confirm'] = array( 'title' => 'Confirm index deletion', 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_delete_index_confirm'), 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_CALLBACK, ); $items['admin/reports/apachesolr'] = array( 'title' => 'Apache Solr search index', 'page callback' => 'apachesolr_index_report', 'access arguments' => array('access site reports'), 'file' => 'apachesolr.admin.inc', ); $items['admin/reports/apachesolr/index'] = array( 'title' => 'Search index', 'file' => 'apachesolr.admin.inc', 'type' => MENU_DEFAULT_LOCAL_TASK, ); $items['admin/settings/apachesolr/mlt/add_block'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_mlt_add_block_form'), 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_CALLBACK, ); $items['admin/settings/apachesolr/mlt/delete_block/%'] = array( 'page callback' => 'drupal_get_form', 'page arguments' => array('apachesolr_mlt_delete_block_form', 5), 'access arguments' => array('administer search'), 'file' => 'apachesolr.admin.inc', 'type' => MENU_CALLBACK, ); return $items; } /** * Determines Apache Solr's behavior when searching causes an exception (e.g. Solr isn't available.) * Depending on the admin settings, possibly redirect to Drupal's core search. * * @param $search_name * The name of the search implementation. * * @param $querystring * The search query that was issued at the time of failure. */ function apachesolr_failure($search_name, $querystring) { $fail_rule = variable_get('apachesolr_failure', 'show_error'); switch ($fail_rule) { case 'show_error': drupal_set_message(t('The Apache Solr search engine is not available. Please contact your site administrator.'), 'error'); break; case 'show_drupal_results': drupal_set_message(t("%search_name is not available. Your search is being redirected.", array('%search_name' => $search_name))); drupal_goto('search/node/' . drupal_urlencode($querystring)); break; case 'show_no_results': return; } } /** * Like $site_key in _update_refresh() - returns a site-specific hash. */ function apachesolr_site_hash() { if (!($hash = variable_get('apachesolr_site_hash', FALSE))) { global $base_url; $hash = substr(md5(md5($base_url . drupal_get_private_key() . 'apachesolr')), 0, 12); variable_set('apachesolr_site_hash', $hash); } return $hash; } /** * Generate a unique ID for an entity being indexed. * * @param $id * An id number (or string) unique to this site, such as a node ID. * @param $entity * A string like 'node', 'file', 'user', or some other Drupal object type. * * @return * A string combining the parameters with the site hash. */ function apachesolr_document_id($id, $entity = 'node') { return apachesolr_site_hash() . "/$entity/" . $id; } /** * Implementation of hook_user(). * * Mark nodes as needing re-indexing if the author name changes. */ function apachesolr_user($op, &$edit, &$account) { switch ($op) { case 'update': if (isset($edit['name']) && $account->name != $edit['name']) { db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE uid = %d)", time(), $account->uid); } break; } } /** * Implementation of hook_taxonomy(). * * Mark nodes as needing re-indexing if a term name changes. */ function apachesolr_taxonomy($op, $type, $edit) { if ($type == 'term' && ($op == 'update')) { db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {term_node} WHERE tid = %d)", time(), $edit['tid']); } // TODO: the rest, such as term deletion. } /** * Implementation of hook_comment(). * * Mark nodes as needing re-indexing if comments are added or changed. * Like search_comment(). */ function apachesolr_comment($edit, $op) { $edit = (array) $edit; switch ($op) { // Reindex the node when comments are added or changed case 'insert': case 'update': case 'delete': case 'publish': case 'unpublish': // TODO: do we want to skip this if we are excluding comments // from the index for this node type? apachesolr_mark_node($edit['nid']); break; } } /** * Mark one node as needing re-indexing. */ function apachesolr_mark_node($nid) { db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid = %d", time(), $nid); } /** * Implementation of hook_node_type(). * * Mark nodes as needing re-indexing if a node type name changes. */ function apachesolr_node_type($op, $info) { if ($op != 'delete' && !empty($info->old_type) && $info->old_type != $info->type) { // We cannot be sure we are going before or after node module. db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s' OR type = '%s')", time(), $info->old_type, $info->type); } } /** * Helper function for modules implmenting hook_search's 'status' op. */ function apachesolr_index_status($namespace) { list($excluded_types, $args, $join_sql, $exclude_sql) = _apachesolr_exclude_types($namespace); $total = db_result(db_query("SELECT COUNT(asn.nid) FROM {apachesolr_search_node} asn ". $join_sql ."WHERE asn.status = 1 " . $exclude_sql, $excluded_types)); $remaining = db_result(db_query("SELECT COUNT(asn.nid) FROM {apachesolr_search_node} asn ". $join_sql ."WHERE (asn.changed > %d OR (asn.changed = %d AND asn.nid > %d)) AND asn.status = 1 " . $exclude_sql, $args)); return array('remaining' => $remaining, 'total' => $total); } /** * Returns last changed and last nid for an indexing namespace. */ function apachesolr_get_last_index($namespace) { $stored = variable_get('apachesolr_index_last', array()); return isset($stored[$namespace]) ? $stored[$namespace] : array('last_change' => 0, 'last_nid' => 0); } /** * Clear a specific namespace's last changed and nid, or clear all. */ function apachesolr_clear_last_index($namespace = '') { if ($namespace) { $stored = variable_get('apachesolr_index_last', array()); unset($stored[$namespace]); variable_set('apachesolr_index_last', $stored); } else { variable_del('apachesolr_index_last'); } } /** * Truncate and rebuild the apachesolr_search_node table, reset the apachesolr_index_last variable. * This is the most complete way to force reindexing, or to build the indexing table for the * first time. * * @param $type * A single content type to be reindexed, leaving the others unaltered. */ function apachesolr_rebuild_index_table($type = NULL) { if (isset($type)) { db_query("DELETE FROM {apachesolr_search_node} WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s')", $type); // Populate table db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed) SELECT n.nid, n.status, %d AS changed FROM {node} n WHERE n.type = '%s'", time(), $type); } else { db_query("DELETE FROM {apachesolr_search_node}"); // Populate table. if (module_exists('comment')) { // If comment module is enabled, use last_comment_timestamp as well. db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed) SELECT n.nid, n.status, GREATEST(n.created, n.changed, c.last_comment_timestamp) AS changed FROM {node} n LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid"); } else { db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed) SELECT n.nid, n.status, GREATEST(n.created, n.changed) AS changed FROM {node} n"); } // Make sure no nodes end up with a timestamp that's in the future. $time = time(); db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE changed > %d", $time, $time); apachesolr_clear_last_index(); } } function _apachesolr_exclude_types($namespace) { extract(apachesolr_get_last_index($namespace)); $excluded_types = module_invoke_all('apachesolr_types_exclude', $namespace); $args = array($last_change, $last_change, $last_nid); $join_sql = ''; $exclude_sql = ''; if ($excluded_types) { $excluded_types = array_unique($excluded_types); $join_sql = "INNER JOIN {node} n ON n.nid = asn.nid "; $exclude_sql = "AND n.type NOT IN(". db_placeholders($excluded_types, 'varchar') .") "; $args = array_merge($args, $excluded_types); } return array($excluded_types, $args, $join_sql, $exclude_sql); } /** * Returns an array of rows from a query based on an indexing namespace. */ function apachesolr_get_nodes_to_index($namespace, $limit) { $rows = array(); if (variable_get('apachesolr_read_only', 0)) { return $rows; } list($excluded_types, $args, $join_sql, $exclude_sql) = _apachesolr_exclude_types($namespace); $result = db_query_range("SELECT asn.nid, asn.changed FROM {apachesolr_search_node} asn ". $join_sql ."WHERE (asn.changed > %d OR (asn.changed = %d AND asn.nid > %d)) AND asn.status = 1 ". $exclude_sql ."ORDER BY asn.changed ASC, asn.nid ASC", $args, 0, $limit); while($row = db_fetch_object($result)) { $rows[] = $row; } return $rows; } /** * Function to handle the indexing of nodes. * * The calling function must supply a name space or track/store * the timestamp and nid returned. * Returns FALSE if no nodes were indexed (none found or error). */ function apachesolr_index_nodes($rows, $namespace = '', $callback = 'apachesolr_add_node_document') { if (!$rows) { // Nothing to do. return FALSE; } try { // Get the $solr object $solr = apachesolr_get_solr(); // If there is no server available, don't continue. if (!$solr->ping(variable_get('apachesolr_ping_timeout', 4))) { throw new Exception(t('No Solr instance available during indexing.')); } } catch (Exception $e) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); return FALSE; } include_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.index.inc'); $documents = array(); $old_position = apachesolr_get_last_index($namespace); $position = $old_position; foreach ($rows as $row) { try { $callback($documents, $row->nid, $namespace); // Variables to track the last item changed. $position['last_change'] = $row->changed; $position['last_nid'] = $row->nid; } catch (Exception $e) { // Something bad happened - don't continue. watchdog('Apache Solr', 'Error constructing documents to index:
!message', array('!message' => nl2br(strip_tags($e->getMessage()))), WATCHDOG_ERROR); break; } } if (count($documents)) { try { watchdog('Apache Solr', 'Adding @count documents.', array('@count' => count($documents))); // Chunk the adds by 20s $docs_chunk = array_chunk($documents, 20); foreach ($docs_chunk as $docs) { $solr->addDocuments($docs); } // Set the timestamp to indicate an index update. apachesolr_index_updated(time()); } catch (Exception $e) { $nids = array(); if (!empty($docs)) { foreach ($docs as $doc) { $nids[] = $doc->nid; } } watchdog('Apache Solr', 'Indexing failed on one of the following nodes: @nids
!message', array('@nids' => implode(', ', $nids), '!message' => nl2br(strip_tags($e->getMessage()))), WATCHDOG_ERROR); return FALSE; } } // Save the new position in case it changed. if ($namespace && $position != $old_position) { $stored = variable_get('apachesolr_index_last', array()); $stored[$namespace] = $position; variable_set('apachesolr_index_last', $stored); } return $position; } /** * Convert date from timestamp into ISO 8601 format. * http://lucene.apache.org/solr/api/org/apache/solr/schema/DateField.html */ function apachesolr_date_iso($date_timestamp) { return gmdate('Y-m-d\TH:i:s\Z', $date_timestamp); } function apachesolr_delete_node_from_index($node) { static $failed = FALSE; if ($failed) { return FALSE; } try { $solr = apachesolr_get_solr(); $solr->deleteById(apachesolr_document_id($node->nid)); apachesolr_index_updated(time()); return TRUE; } catch (Exception $e) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); // Don't keep trying queries if they are failing. $failed = TRUE; return FALSE; } } /** * Helper function to keep track of when the index has been updated. */ function apachesolr_index_updated($updated = NULL) { if (isset($updated)) { if ($updated) { variable_set('apachesolr_index_updated', (int) $updated); } else { variable_del('apachesolr_index_updated'); } } return variable_get('apachesolr_index_updated', 0); } /** * Implementation of hook_cron(). */ function apachesolr_cron() { // Mass update and delete functions are in the include file. include_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.index.inc'); apachesolr_cron_check_node_table(); try { $solr = apachesolr_get_solr(); // Optimize the index (by default once a day). $optimize_interval = variable_get('apachesolr_optimize_interval', 60 * 60 * 24); $last = variable_get('apachesolr_last_optimize', 0); $time = time(); if ($optimize_interval && ($time - $last > $optimize_interval)) { $solr->optimize(FALSE, FALSE); variable_set('apachesolr_last_optimize', $time); apachesolr_index_updated($time); } // Only clear the cache if the index changed. // TODO: clear on some schedule if running multi-site. $updated = apachesolr_index_updated(); if ($updated) { $solr->clearCache(); // Re-populate the luke cache. $solr->getLuke(); // TODO: an admin interface for setting this. Assume for now 5 minutes. if ($time - $updated >= variable_get('apachesolr_cache_delay', 300)) { // Clear the updated flag. apachesolr_index_updated(FALSE); } } } catch (Exception $e) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())) . ' in apachesolr_cron', NULL, WATCHDOG_ERROR); } } /** * Implementation of hook_nodeapi(). */ function apachesolr_nodeapi(&$node, $op, $a3 = NULL, $a4 = NULL) { switch ($op) { case 'delete': _apachesolr_nodeapi_delete($node); break; case 'insert': // Make sure no node ends up with a timestamp that's in the future // by using time() rather than the node's changed or created timestamp. db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed) VALUES (%d, %d, %d)", $node->nid, $node->status, time()); break; case 'update': _apachesolr_nodeapi_update($node); break; } } /** * Helper function for hook_nodeapi(). */ function _apachesolr_nodeapi_delete($node, $set_message = TRUE) { if (apachesolr_delete_node_from_index($node)) { // There was no exception, so delete from the table. db_query("DELETE FROM {apachesolr_search_node} WHERE nid = %d", $node->nid); if ($set_message && user_access('administer search') && variable_get('apachesolr_set_nodeapi_messages', 1)) { apachesolr_set_stats_message('Deleted content will be removed from the Apache Solr search index in approximately @autocommit_time.'); } } } /** * Helper function for hook_nodeapi(). */ function _apachesolr_nodeapi_update($node, $set_message = TRUE) { // Check if the node has gone from published to unpublished. if (!$node->status && db_result(db_query("SELECT status FROM {apachesolr_search_node} WHERE nid = %d", $node->nid))) { if (apachesolr_delete_node_from_index($node)) { // There was no exception, so update the table. db_query("UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d", time(), $node->status, $node->nid); if ($set_message && user_access('administer search') && variable_get('apachesolr_set_nodeapi_messages', 1)) { apachesolr_set_stats_message('Unpublished content will be removed from the Apache Solr search index in approximately @autocommit_time.'); } } } else { db_query("UPDATE {apachesolr_search_node} SET changed = %d, status = %d WHERE nid = %d", time(), $node->status, $node->nid); } } /** * Call drupal_set_message() with the text. * * The text is translated with t() and substituted using Solr stats. */ function apachesolr_set_stats_message($text, $type = 'status', $repeat = FALSE) { try { $solr = apachesolr_get_solr(); $stats_summary = $solr->getStatsSummary(); drupal_set_message(t($text, $stats_summary), $type, FALSE); } catch (Exception $e) { watchdog('apachesolr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); } } /** * Return the enabled facets from the specified block array. * * @param $module * The module (optional). * @return * An array consisting of info for facets that have been enabled * for the specified module, or all enabled facets. */ function apachesolr_get_enabled_facets($module = NULL) { $enabled = variable_get('apachesolr_enabled_facets', array()); if (isset($module)) { return isset($enabled[$module]) ? $enabled[$module] : array(); } return $enabled; } /** * Save the enabled facets for all modules. * * @param $enabled * An array consisting of info for all enabled facets. * @return * The array consisting of info for all enabled facets. */ function apachesolr_save_enabled_facets($enabled) { variable_set('apachesolr_enabled_facets', $enabled); return $enabled; } /** * Save the enabled facets for one module. * * @param $module * The module name. * @param $facets * Associative array of $delta => $facet_field pairs. If omitted, all facets * for $module are disabled. * @return * An array consisting of info for all enabled facets. */ function apachesolr_save_module_facets($module, $facets = array()) { $enabled = variable_get('apachesolr_enabled_facets', array()); if (!empty($facets) && is_array($facets)) { $enabled[$module] = $facets; } else { unset($enabled[$module]); } variable_set('apachesolr_enabled_facets', $enabled); return $enabled; } /** * Implementation of hook_block(). */ function apachesolr_block($op = 'list', $delta = 0, $edit = array()) { static $access; switch ($op) { case 'list': // Get all of the moreLikeThis blocks that the user has created $blocks = apachesolr_mlt_list_blocks(); // Add the sort block. $blocks['sort'] = array( 'info' => t('Apache Solr Core: Sorting'), 'cache' => BLOCK_CACHE_PER_PAGE, ); return $blocks; case 'view': if ($delta != 'sort' && ($node = menu_get_object()) && (!arg(2) || arg(2) == 'view')) { $suggestions = array(); // Determine whether the user can view the current node. if (!isset($access)) { $access = node_access('view', $node); } $block = apachesolr_mlt_load_block($delta); if ($access && $block) { $docs = apachesolr_mlt_suggestions($block, apachesolr_document_id($node->nid)); if (!empty($docs)) { $suggestions['subject'] = check_plain($block['name']); $suggestions['content'] = theme('apachesolr_mlt_recommendation_block', $docs); if (user_access('administer search')) { $suggestions['content'] .= l(t('Configure this block'),'admin/build/block/configure/apachesolr/' . $delta, array('attributes' => array('class' => 'apachesolr-mlt-admin-link'))); } } } return $suggestions; } elseif (apachesolr_has_searched() && $delta == 'sort') { // Get the query and response. Without these no blocks make sense. $response = apachesolr_static_response_cache(); if (empty($response) || ($response->response->numFound < 2)) { return; } $query = apachesolr_current_query(); $sorts = $query->get_available_sorts(); // Get the current sort as an array. $solrsort = $query->get_solrsort(); $sort_links = array(); $path = $query->get_path(); $new_query = clone $query; $toggle = array('asc' => 'desc', 'desc' => 'asc'); foreach ($sorts as $name => $sort) { $active = $solrsort['#name'] == $name; $direction = ''; $new_direction = $sort['default']; if ($name == 'score') { // We only sort by ascending score. $new_direction = 'asc'; } elseif ($active) { $direction = $solrsort['#direction']; $new_direction = $toggle[$solrsort['#direction']]; } $new_query->set_solrsort($name, $new_direction); $sort_links[$name] = array( 'title' => $sort['title'], 'path' => $path, 'options' => array('query' => $new_query->get_url_queryvalues()), 'active' => $active, 'direction' => $direction, ); } // Allow other modules to add or remove sorts. drupal_alter('apachesolr_sort_links', $sort_links); foreach ($sort_links as $name => $link) { $themed_links[$name] = theme('apachesolr_sort_link', $link['title'], $link['path'], $link['options'], $link['active'], $link['direction']); } return array('subject' => t('Sort by'), 'content' => theme('apachesolr_sort_list', $themed_links)); } break; case 'configure': if ($delta != 'sort') { require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc'); return apachesolr_mlt_block_form($delta); } break; case 'save': if ($delta != 'sort') { require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc'); apachesolr_mlt_save_block($edit, $delta); } break; } } /** * Helper function for displaying a facet block. */ function apachesolr_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) { if (!empty($response->facet_counts->facet_fields->$facet_field)) { $contains_active = FALSE; $items = array(); foreach ($response->facet_counts->facet_fields->$facet_field as $facet => $count) { $sortpre = 1000000 - $count; $options = array(); $exclude = FALSE; // Solr sends this back if it's empty. if ($facet == '_empty_') { $exclude = TRUE; $facet = '[* TO *]'; $facet_text = theme('placeholder', t('Missing this field')); $options['html'] = TRUE; // Put this just below any active facets. // '-' sorts before all numbers, but after '*'. $sortpre = '-'; } else { $facet_text = $facet; } if ($facet_callback && function_exists($facet_callback)) { $facet_text = $facet_callback($facet, $options); } $unclick_link = ''; $active = FALSE; $new_query = clone $query; if ($query->has_filter($facet_field, $facet)) { $contains_active = $active = TRUE; // '*' sorts before all numbers. $sortpre = '*'; $new_query->remove_filter($facet_field, $facet); $options['query'] = $new_query->get_url_queryvalues(); $link = theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options); } else { $new_query->add_filter($facet_field, $facet, $exclude); $options['query'] = $new_query->get_url_queryvalues(); $link = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, FALSE, $response->response->numFound); } if ($count || $active) { $items[$sortpre . '*' . $facet_text] = $link; } } // Unless a facet is active only display 2 or more. if ($items && ($response->response->numFound > 1 || $contains_active)) { ksort($items, SORT_STRING); // Get information needed by the rest of the blocks about limits. $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array()); $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10); $output = theme('apachesolr_facet_list', $items, $limit); return array('subject' => $filter_by, 'content' => $output); } } return NULL; } /** * Helper function for displaying a date facet block. * * TODO: Refactor with apachesolr_facet_block(). */ function apachesolr_date_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) { $items = array(); $new_query = clone $query; foreach (array_reverse($new_query->get_filters($facet_field)) as $filter) { $options = array(); // Iteratively remove the date facets. $new_query->remove_filter($facet_field, $filter['#value']); if ($facet_callback && function_exists($facet_callback)) { $facet_text = $facet_callback($filter['#start'], $options); } else { $facet_text = apachesolr_date_format_iso_by_gap(apachesolr_date_find_query_gap($filter['#start'], $filter['#end']), $filter['#start']); } $options['query'] = $new_query->get_url_queryvalues(); array_unshift($items, theme('apachesolr_unclick_link', $facet_text, $new_query->get_path(), $options)); } // Add links for additional date filters. if (!empty($response->facet_counts->facet_dates->$facet_field)) { $field = clone $response->facet_counts->facet_dates->$facet_field; $end = $field->end; unset($field->end); $gap = $field->gap; unset($field->gap); // Treat each date facet as a range start, and use the next date // facet as range end. Use 'end' for the final end. $range_end = array(); foreach ($field as $facet => $count) { if (isset($prev_facet)) { $range_end[$prev_facet] = $facet; } $prev_facet = $facet; } $range_end[$prev_facet] = $end; foreach ($field as $facet => $count) { $options = array(); // Solr sends this back if it's empty. if ($facet == '_empty_' || $count == 0) { continue; } if ($facet_callback && function_exists($facet_callback)) { $facet_text = $facet_callback($facet, $options); } else { $facet_text = apachesolr_date_format_iso_by_gap(substr($gap, 2), $facet); } $new_query = clone $query; $new_query->add_filter($facet_field, '['. $facet .' TO '. $range_end[$facet] .']'); $options['query'] = $new_query->get_url_queryvalues(); $items[] = theme('apachesolr_facet_link', $facet_text, $new_query->get_path(), $options, $count, FALSE, $response->response->numFound); } } if (count($items) > 0) { // Get information needed by the rest of the blocks about limits. $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array()); $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10); $output = theme('apachesolr_facet_list', $items, $limit); return array('subject' => $filter_by, 'content' => $output); } return NULL; } /** * Determine the gap in a date range query filter that we generated. * * This function assumes that the start and end dates are the * beginning and end of a single period: 1 year, month, day, hour, * minute, or second (all date range query filters we generate meet * this criteria). So, if the seconds are different, it is a second * gap. If the seconds are the same (incidentally, they will also be * 0) but the minutes are different, it is a minute gap. If the * minutes are the same but hours are different, it's an hour gap. * etc. * * @param $start * Start date as an ISO date string. * @param $end * End date as an ISO date string. * @return * YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND. */ function apachesolr_date_find_query_gap($start_iso, $end_iso) { $gaps = array('SECOND' => 6, 'MINUTE' => 5, 'HOUR' => 4, 'DAY' => 3, 'MONTH' => 2, 'YEAR' => 1); $re = '@(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})@'; if (preg_match($re, $start_iso, $start) && preg_match($re, $end_iso, $end)) { foreach ($gaps as $gap => $idx) { if ($start[$idx] != $end[$idx]) { return $gap; } } } // can't tell return 'YEAR'; } /** * Format an ISO date string based on the gap used to generate it. * * This function assumes that gaps less than one day will be displayed * in a search context in which a larger containing gap including a * day is already displayed. So, HOUR, MINUTE, and SECOND gaps only * display time information, without date. * * @param $gap * A gap. * @param $iso * An ISO date string. * @return * A gap-appropriate formatted date. */ function apachesolr_date_format_iso_by_gap($gap, $iso) { // TODO: If we assume that multiple search queries are formatted in // order, we could store a static list of all gaps we've formatted. // Then, if we format an HOUR, MINUTE, or SECOND without previously // having formatted a DAY or later, we could include date // information. However, we'd need to do that per-field and I'm not // our callers always have field information handy. $unix = strtotime($iso); if ($unix > 0) { switch ($gap) { case 'YEAR': return gmdate('Y', $unix); case 'MONTH': return gmdate('F Y', $unix); case 'DAY': return gmdate('F j, Y', $unix); case 'HOUR': return gmdate('g A', $unix); case 'MINUTE': return gmdate('g:i A', $unix); case 'SECOND': return gmdate('g:i:s A', $unix); } } return $iso; } /** * Format the beginning of a date range query filter that we * generated. * * @param $start_iso * The start date. * @param $end_iso * The end date. * @return * A display string reprepsenting the date range, such as "January * 2009" for "2009-01-01T00:00:00Z TO 2009-02-01T00:00:00Z" */ function apachesolr_date_format_range($start_iso, $end_iso) { $gap = apachesolr_date_find_query_gap($start_iso, $end_iso); return apachesolr_date_format_iso_by_gap($gap, $start_iso); } /** * Determine the best search gap to use for an arbitrary date range. * * Generally, we the maximum gap that fits between the start and end * date. If they are more than a year apart, 1 year; if they are more * than a month apart, 1 month; etc. * * This function uses Unix timestamps for its computation and so is * not useful for dates outside that range. * * @param $start * Start date as an ISO date string. * @param $end * End date as an ISO date string. * @return * YEAR, MONTH, DAY, HOUR, MINUTE, or SECOND depending on how far * apart $start and $end are. */ function apachesolr_date_determine_gap($start, $end) { $start = strtotime($start); $end = strtotime($end); if ($end - $start >= 86400*365) { return 'YEAR'; } if (date('m', $start) != date('m', $end)) { return 'MONTH'; } if ($end - $start > 86400) { return 'DAY'; } // For now, HOUR is a reasonable smallest gap. return 'HOUR'; } /** * Return the next smaller date gap. * * @param $gap * A gap. * @return * The next smaller gap, or NULL if there is no smaller gap. */ function apachesolr_date_gap_drilldown($gap) { $drill = array( 'YEAR' => 'MONTH', 'MONTH' => 'DAY', 'DAY' => 'HOUR', // For now, HOUR is a reasonable smallest gap. // 'HOUR' => 'MINUTE', ); return isset($drill[$gap]) ? $drill[$gap] : NULL; } /** * Used by the 'configure' $op of hook_block so that modules can generically set * facet limits on their blocks. */ function apachesolr_facetcount_form($module, $delta) { $initial = variable_get('apachesolr_facet_query_initial_limits', array()); $limits = variable_get('apachesolr_facet_query_limits', array()); $facet_missing = variable_get('apachesolr_facet_missing', array()); $limit = drupal_map_assoc(array(50, 40, 30, 20, 15, 10, 5, 3)); $form['apachesolr_facet_query_initial_limit'] = array( '#type' => 'select', '#title' => t('Initial filter links'), '#options' => $limit, '#description' => t('The initial number of filter links to show in this block.'), '#default_value' => isset($initial[$module][$delta]) ? $initial[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10), ); $limit = drupal_map_assoc(array(100, 75, 50, 40, 30, 20, 15, 10, 5, 3)); $form['apachesolr_facet_query_limit'] = array( '#type' => 'select', '#title' => t('Maximum filter links'), '#options' => $limit, '#description' => t('The maximum number of filter links to show in this block.'), '#default_value' => isset($limits[$module][$delta]) ? $limits[$module][$delta] : variable_get('apachesolr_facet_query_limit_default', 20), ); $form['apachesolr_facet_missing'] = array( '#type' => 'radios', '#title' => t('Include a facet for missing'), '#options' => array(0 => t('No'), 1 => t('Yes')), '#description' => t('A facet can be generated corresponding to all documents entirely missing this field.'), '#default_value' => isset($facet_missing[$module][$delta]) ? $facet_missing[$module][$delta] : 0, ); return $form; } /** * Used by the 'save' $op of hook_block so that modules can generically set * facet limits on their blocks. */ function apachesolr_facetcount_save($edit) { // Save query limits $module = $edit['module']; $delta = $edit['delta']; $limits = variable_get('apachesolr_facet_query_limits', array()); $limits[$module][$delta] = (int)$edit['apachesolr_facet_query_limit']; variable_set('apachesolr_facet_query_limits', $limits); $initial = variable_get('apachesolr_facet_query_initial_limits', array()); $initial[$module][$delta] = (int)$edit['apachesolr_facet_query_initial_limit']; variable_set('apachesolr_facet_query_initial_limits', $initial); $facet_missing = variable_get('apachesolr_facet_missing', array()); $facet_missing[$module][$delta] = (int)$edit['apachesolr_facet_missing']; variable_set('apachesolr_facet_missing', $facet_missing); } /** * Initialize a pager for theme('pager') without running an SQL query. * * @see pager_query() * * @param $total * The total number of items found. * @param $limit * The number of items you will display per page. * @param $element * An optional integer to distinguish between multiple pagers on one page. * * @return * The current page for $element. 0 by default if $_GET['page'] is empty. */ function apachesolr_pager_init($total, $limit = 10, $element = 0) { global $pager_page_array, $pager_total, $pager_total_items; $page = isset($_GET['page']) ? $_GET['page'] : ''; // Convert comma-separated $page to an array, used by other functions. $pager_page_array = explode(',', $page); // We calculate the total of pages as ceil(items / limit). $pager_total_items[$element] = $total; $pager_total[$element] = ceil($pager_total_items[$element] / $limit); $pager_page_array[$element] = max(0, min((int)$pager_page_array[$element], ((int)$pager_total[$element]) - 1)); return $pager_page_array[$element]; } /** * This hook allows modules to modify the query and params objects. * * Example: * * function my_module_apachesolr_modify_query(&$query, &$params) { * // I only want to see articles by the admin! * $query->add_filter("uid", 1); * * } */ function apachesolr_modify_query(&$query, &$params, $caller) { if (empty($query)) { // This should only happen if Solr is not set up - avoids fatal errors. return; } foreach (module_implements('apachesolr_modify_query') as $module) { $function_name = $module . '_apachesolr_modify_query'; $function_name($query, $params, $caller); } // TODO: The query object should hold all the params. // Add array of fq parameters. if ($query && ($fq = $query->get_fq())) { $params['fq'] = $fq; } // Add sort if present. if ($query) { $sort = $query->get_solrsort(); $sortstring = $sort['#name'] .' '. $sort['#direction']; // We don't bother telling Solr to do its default sort. if ($sortstring != 'score asc') { $params['sort'] = $sortstring; } } } /** * Semaphore that indicates whether a search has been done. Blocks use this * later to decide whether they should load or not. * * @param $searched * A boolean indicating whether a search has been executed. * * @return * TRUE if a search has been executed. * FALSE otherwise. */ function apachesolr_has_searched($searched = NULL) { static $_searched = FALSE; if (is_bool($searched)) { $_searched = $searched; } return $_searched; } /** * Factory method for solr singleton object. Structure allows for an arbitrary * number of solr objects to be used based on the host, port, path combination. * Get an instance like this: * $solr = apachesolr_get_solr(); */ function apachesolr_get_solr($host = NULL, $port = NULL, $path = NULL) { static $solr_cache; if (empty($host)) { $host = variable_get('apachesolr_host', 'localhost'); } if (empty($port)) { $port = variable_get('apachesolr_port', '8983'); } if (empty($path)) { $path = variable_get('apachesolr_path', '/solr'); } if (empty($solr_cache[$host][$port][$path])) { list($module, $filepath, $class) = variable_get('apachesolr_service_class', array('apachesolr', 'Drupal_Apache_Solr_Service.php', 'Drupal_Apache_Solr_Service')); include_once(drupal_get_path('module', $module) .'/'. $filepath); try { $solr_cache[$host][$port][$path] = new $class($host, $port, $path); } catch (Exception $e) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); return; } } return $solr_cache[$host][$port][$path]; } /** * It is important to hold on to the Solr response object for the duration of the * page request so that we can use it for things like building facet blocks. */ function apachesolr_static_response_cache($response = NULL) { static $_response; if (!empty($response)) { $_response = clone $response; } return $_response; } /** * Factory function for query objects. * * @param $keys * The string that a user would type into the search box. Suitable input * may come from search_get_keys(). * * @param $filters * Key and value pairs that are applied as a filter query. * * @param $solrsort * Visible string telling solr how to sort. * * @param $base_path * The search base path (without the keywords) for this query. */ function apachesolr_drupal_query($keys = '', $filters = '', $solrsort = '', $base_path = '') { list($module, $class) = variable_get('apachesolr_query_class', array('apachesolr', 'Solr_Base_Query')); include_once drupal_get_path('module', $module) .'/'. $class .'.php'; try { $query = new $class(apachesolr_get_solr(), $keys, $filters, $solrsort, $base_path); } catch (Exception $e) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR); $query = NULL; } return $query; } /** * Static getter/setter for the current query */ function apachesolr_current_query($query = NULL) { static $saved_query = NULL; if (is_object($query)) { $saved_query = clone $query; } return empty($saved_query) ? $saved_query : clone $saved_query; } /** * array('index_type' => 'integer', * 'multiple' => TRUE, * 'name' => 'fieldname', * ), */ function apachesolr_index_key($field) { switch ($field['index_type']) { case 'text': $type_prefix = 't'; break; case 'string': $type_prefix = 's'; break; case 'integer': $type_prefix = 'i'; break; case 'sint': $type_prefix = 'si'; break; case 'double': $type_prefix = 'p'; // reserve d for date break; case 'boolean': $type_prefix = 'b'; break; case 'date': $type_prefix = 'd'; break; case 'float': $type_prefix = 'f'; break; case 'tdate': $type_prefix = 'td'; break; case 'tint': $type_prefix = 'ti'; break; case 'tlong'; $type_prefix = 'tl'; break; case 'tfloat': $type_prefix = 'tf'; break; case 'tdouble': $type_prefix = 'tp'; break; default: $type_prefix = 's'; } $sm = $field['multiple'] ? 'm_' : 's_'; return $type_prefix . $sm . $field['name']; } /** * Try to map a schema field name to a human-readable description. */ function apachesolr_field_name_map($field_name) { require_once(drupal_get_path('module', 'apachesolr') .'/apachesolr.admin.inc'); return _apachesolr_field_name_map($field_name); } /** * Invokes hook_apachesolr_cck_field_mappings to find out how to handle CCK fields. */ function apachesolr_cck_fields() { static $fields; if (is_null($fields)) { $fields = array(); // If CCK isn't enabled, do nothing. if (module_exists('content')) { // A single default mapping for all text fields. $mappings['text'] = array( 'optionwidgets_select' => array('callback' => '', 'index_type' => 'string'), 'optionwidgets_buttons' => array('callback' => '', 'index_type' => 'string') ); // A field-specific mapping would look like: // $mappings['per-field']['field_model_name'] = array('callback' => '', 'index_type' => 'string'); // or // $mappings['per-field']['field_model_price'] = array('callback' => '', 'index_type' => 'float'); // Allow other modules to add or alter mappings. drupal_alter('apachesolr_cck_fields', $mappings); $result = db_query("SELECT i.field_name, f.multiple, f.type AS field_type, i.widget_type, i.label FROM {content_node_field_instance} i INNER JOIN {content_node_field} f ON i.field_name = f.field_name;"); while ($row = db_fetch_object($result)) { // Only deal with fields that have option widgets (facets don't make sense otherwise), or fields that have specific mappings. if ((isset($mappings[$row->field_type][$row->widget_type]) && !isset($fields[$row->field_name])) || isset($mappings['per-field'][$row->field_name])) { if (isset($mappings['per-field'][$row->field_name])) { $row->index_type = $mappings['per-field'][$row->field_name]['index_type']; $row->callback = $mappings['per-field'][$row->field_name]['callback']; } else { $row->index_type = $mappings[$row->field_type][$row->widget_type]['index_type']; $row->callback = $mappings[$row->field_type][$row->widget_type]['callback']; } $row->multiple = (bool) $row->multiple; $row->name = 'cck_' . $row->field_name; $fields[$row->field_name] = (array) $row; } } } } return $fields; } /** * Implementation of hook_theme(). */ function apachesolr_theme() { return array( /** * Returns a link for a facet term, with the number (count) of results for that term */ 'apachesolr_facet_link' => array( 'arguments' => array('facet_text' => NULL, 'path' => NULL, 'options' => NULL, 'count' => NULL, 'active' => FALSE, 'num_found' => NULL), ), /** * Returns a link to remove a facet filter from the current search. */ 'apachesolr_unclick_link' => array( 'arguments' => array('facet_text' => NULL, 'path' => NULL, 'options' => NULL), ), /** * Returns a list of links from the above functions (apachesolr_facet_item and apachesolr_unclick_link) */ 'apachesolr_facet_list' => array( 'arguments' => array('items' => NULL, 'display_limit' => NULL), ), /** * Returns a list of links generated by apachesolr_sort_link */ 'apachesolr_sort_list' => array( 'arguments' => array('items' => NULL), ), /** * Returns a link which can be used to search the results. */ 'apachesolr_sort_link' => array( 'arguments' => array('text' => NULL, 'path' => NULL, 'options' => NULL, 'active' => FALSE, 'direction' => ''), ), /** * Returns a list of results (docs) in content recommendation block */ 'apachesolr_mlt_recommendation_block' => array( 'arguments' => array('docs' => NULL), ), ); } /** * Performs a moreLikeThis query using the settings and retrieves documents. * * @param $settings * An array of settings. * @param $id * The Solr ID of the document for which you want related content. * For a node that is apachesolr_document_id($node->nid) * * @return An array of response documents, or NULL */ function apachesolr_mlt_suggestions($settings, $id) { try { $solr = apachesolr_get_solr(); $fields = array( 'mlt_mintf' => 'mlt.mintf', 'mlt_mindf' => 'mlt.mindf', 'mlt_minwl' => 'mlt.minwl', 'mlt_maxwl' => 'mlt.maxwl', 'mlt_maxqt' => 'mlt.maxqt', 'mlt_boost' => 'mlt.boost', 'mlt_qf' => 'mlt.qf', ); $params = array( 'qt' => 'mlt', 'fl' => 'nid,title,path,url', 'mlt.fl' => implode(',', $settings['mlt_fl']), ); foreach ($fields as $form_key => $name) { if (!empty($settings[$form_key])) { $params[$name] = $settings[$form_key]; } } $query = apachesolr_drupal_query('id:' . $id); // This hook allows modules to modify the query and params objects. apachesolr_modify_query($query, $params, 'apachesolr_mlt'); if (empty($query)) { return; } $response = $solr->search($query->get_query_basic(), 0, $settings['num_results'], $params); if ($response->response) { $docs = (array) end($response->response); return $docs; } } catch ( Exception $e ) { watchdog('Apache Solr', nl2br(check_plain($e->getMessage())), NULL, WATCHDOG_ERROR ); } } /** * Implementation of hook_form_[form_id]_alter */ function apachesolr_form_block_admin_display_form_alter(&$form) { foreach ($form as $key => $block) { if ((strpos($key, "apachesolr_mlt-") === 0) && $block['module']['#value'] == 'apachesolr') { $form[$key]['delete'] = array('#value' => l(t('delete'), 'admin/settings/apachesolr/mlt/delete_block/'. $block['delta']['#value'])); } } } /** * Implementation of hook_form_[form_id]_alter(). * * Hide the core 'title' field in favor of our 'name' field.. */ function apachesolr_form_block_admin_configure_alter(&$form, $form_state) { if ($form['module']['#value'] == 'apachesolr' && $form['delta']['#value'] != 'sort') { $form['block_settings']['title']['#access'] = FALSE; } } /** * Returns a list of blocks. Used by hook_block */ function apachesolr_mlt_list_blocks() { $blocks = variable_get('apachesolr_mlt_blocks', array()); foreach ($blocks as $delta => $settings) { $blocks[$delta] += array('info' => t('Apache Solr recommendations: !name', array('!name' => $settings['name'])) , 'cache' => BLOCK_CACHE_PER_PAGE); } return $blocks; } function apachesolr_mlt_load_block($delta) { $blocks = variable_get('apachesolr_mlt_blocks', array()); return isset($blocks[$delta]) ? $blocks[$delta] : FALSE; } function theme_apachesolr_mlt_recommendation_block($docs) { $links = array(); foreach ($docs as $result) { // Suitable for single-site mode. $links[] = l($result->title, $result->path, array('html' => TRUE)); } return theme('item_list', $links); } function theme_apachesolr_facet_link($facet_text, $path, $options = array(), $count, $active = FALSE, $num_found = NULL) { $options['attributes']['class'][] = 'apachesolr-facet'; if ($active) { $options['attributes']['class'][] = 'active'; } $options['attributes']['class'] = implode(' ', $options['attributes']['class']); return apachesolr_l($facet_text ." ($count)", $path, $options); } /** * A replacement for l() * - doesn't add the 'active' class * - retains all $_GET parameters that ApacheSolr may not be aware of * - if set, $options['query'] MUST be an array * * @see http://api.drupal.org/api/function/l/6 for parameters and options. * * @return * an HTML string containing a link to the given path. */ function apachesolr_l($text, $path, $options = array()) { // Merge in defaults. $options += array( 'attributes' => array(), 'html' => FALSE, 'query' => array(), ); // Don't need this, and just to be safe. unset($options['attributes']['title']); // Double encode + characters for clean URL Apache quirks. if (variable_get('clean_url', '0')) { $path = str_replace('+', '%2B', $path); } // Retain GET parameters that ApacheSolr knows nothing about. $query = apachesolr_current_query(); $get = array_diff_key($_GET, array('q' => 1, 'page' => 1), $options['query'], $query->get_url_queryvalues()); $options['query'] += $get; return ''. ($options['html'] ? $text : check_plain($text)) .''; } function theme_apachesolr_unclick_link($facet_text, $path, $options = array()) { if (empty($options['html'])) { $facet_text = check_plain($facet_text); } else { // Don't pass this option as TRUE into apachesolr_l(). unset($options['html']); } $options['attributes']['class'] = 'apachesolr-unclick'; return apachesolr_l("(-)", $path, $options) . ' '. $facet_text; } function theme_apachesolr_sort_link($text, $path, $options = array(), $active = FALSE, $direction = '') { $icon = ''; if ($direction) { $icon = ' '. theme('tablesort_indicator', $direction); } if ($active) { if (isset($options['attributes']['class'])) { $options['attributes']['class'] .= ' active'; } else { $options['attributes']['class'] = 'active'; } } return $icon . apachesolr_l($text, $path, $options); } function theme_apachesolr_facet_list($items, $display_limit = 0) { // theme('item_list') expects a numerically indexed array. $items = array_values($items); // If there is a limit and the facet count is over the limit, hide the rest. if (($display_limit > 0) && (count($items) > $display_limit)) { // Show/hide extra facets. drupal_add_js(drupal_get_path('module', 'apachesolr') . '/apachesolr.js'); // Split items array into displayed and hidden. $hidden_items = array_splice($items, $display_limit); foreach ($hidden_items as $hidden_item) { if (!is_array($hidden_item)) { $hidden_item = array('data' => $hidden_item); } $items[] = $hidden_item + array('class' => 'apachesolr-hidden-facet'); } } $admin_link = ''; if (user_access('administer search')) { $admin_link = l(t('Configure enabled filters'), 'admin/settings/apachesolr/enabled-filters'); } return theme('item_list', $items) . $admin_link; } function theme_apachesolr_sort_list($items) { // theme('item_list') expects a numerically indexed array. $items = array_values($items); return theme('item_list', $items); } /** * The interface for all 'query' objects. */ interface Drupal_Solr_Query_Interface { /** * Checks to see if a specific filter is already present. * * @param string $field * the facet field to check * * @param string $value * The facet value to check against */ function has_filter($field, $value); /** * Remove a filter from the query * * @param string $field * the facet field to remove * * @param string $value * The facet value to remove * This value can be NULL */ function remove_filter($field, $value = NULL); /** * Add a filter to a query * * @param string $field * the facet field to apply to this query * * @param string value * the value of the facet to apply * * @param boolean $exclude * Optional paramter. If TRUE, the filter will be negative, * meaning that matching values will be excluded from the * result set. */ function add_filter($field, $value, $exclude = FALSE); /** * Return the search path (including the search keywords). */ function get_path(); /** * Return an array of parameters for use in the l function. * * @see l() */ function get_url_queryvalues(); /** * return the basic string query */ function get_query_basic(); /** * Set the solrsort. * * @param $field * The name of a field in the solr index that's an allowed sort. * * @param $direction * 'asc' or 'desc' */ function set_solrsort($field, $direction); /** * Get the solrsort. * * Returns the non-urlencode, non-aliased sort field and direction. * as an array keyed with '#name' and '#direction'. */ function get_solrsort(); /** * Return an array of all filters. */ function get_filters($name = NULL); /** * Add a subquery to the query. * * @param Drupal_Solr_Query_Interface $query * The query to add to the orginal query - may have keywords or filters. * * @param string $fq_operator * The operator to use within the filter part of the subquery * * @param string $q_operator * The operator to use in joining the subquery to * the main keywords. Note - this is unlikely to work * with the Dismax handler when the main query is only * keywords. */ function add_subquery(Drupal_Solr_Query_Interface $query, $fq_operator = 'OR', $q_operator = 'AND'); /** * return the sorts that are provided by the query object * * @return array all the sorts provided */ function get_available_sorts(); /** * Remove a specific subquery * * @param Drupal_Solr_Query_Interface $query * the query to remove */ function remove_subquery(Drupal_Solr_Query_Interface $query); /** * remove all subqueries */ function remove_subqueries(); /** * make a sort available */ function set_available_sort($field, $sort); }