You are here

apachesolr.module in Apache Solr Search 5.2

Integration with the Apache Solr search application.

File

apachesolr.module
View source
<?php

/**
 * @file
 *   Integration with the Apache Solr search application.
 */
define('APACHESOLR_READ_WRITE', 0);
define('APACHESOLR_READ_ONLY', 1);
include_once drupal_get_path('module', 'apachesolr') . '/apachesolr.d6.inc';

// Include admin functions only on the admin pages.
if (arg(0) == 'admin' && (arg(1) == 'settings' || arg(1) == 'logs') && arg(2) == 'apachesolr') {
  include_once drupal_get_path('module', 'apachesolr') . '/apachesolr.admin.inc';
}

/**
 * Implementation of hook_menu().
 */
function apachesolr_menu($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/settings/apachesolr',
      'title' => t('Apache Solr'),
      'description' => t('Administer Apache Solr.'),
      'callback' => 'drupal_get_form',
      'callback arguments' => 'apachesolr_settings',
      'access' => user_access('administer search'),
    );
    $items[] = array(
      'path' => 'admin/settings/apachesolr/settings',
      'title' => t('Settings'),
      'weight' => -10,
      'access' => user_access('administer search'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/settings/apachesolr/enabled-filters',
      'title' => t('Enabled filters'),
      'callback' => 'drupal_get_form',
      'callback arguments' => 'apachesolr_enabled_facets_form',
      'weight' => -7,
      'access' => user_access('administer search'),
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/settings/apachesolr/index',
      'title' => t('Search index'),
      'callback' => 'apachesolr_index_page',
      'access' => user_access('administer search'),
      'weight' => -8,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/settings/apachesolr/index/clear/confirm',
      'title' => t('Confirm the re-indexing of all content'),
      'callback' => 'drupal_get_form',
      'callback arguments' => 'apachesolr_clear_index_confirm',
      'access' => user_access('administer search'),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/settings/apachesolr/index/delete/confirm',
      'title' => t('Confirm index deletion'),
      'callback' => 'drupal_get_form',
      'callback arguments' => 'apachesolr_delete_index_confirm',
      'access' => user_access('administer search'),
      'type' => MENU_CALLBACK,
    );
    $items[] = array(
      'path' => 'admin/logs/apachesolr',
      'title' => t('Apache Solr search index'),
      'callback' => 'apachesolr_index_report',
      'access' => user_access('access site reports'),
    );
    $items[] = array(
      'path' => 'admin/logs/apachesolr/index',
      'title' => t('Search index'),
      'type' => MENU_DEFAULT_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/settings/apachesolr/mlt/add_block',
      'callback' => 'drupal_get_form',
      'callback arguments' => 'apachesolr_mlt_add_block_form',
      'access' => user_access('administer search'),
      'type' => MENU_CALLBACK,
    );
  }
  else {
    if (arg(0) == 'admin' && arg(1) == 'settings' && arg(2) == 'apachesolr' && arg(3) == 'mlt' && arg(4) == 'delete_block' && arg(5)) {
      $items[] = array(
        'path' => 'admin/settings/apachesolr/mlt/delete_block/' . arg(5),
        'callback' => 'drupal_get_form',
        'callback arguments' => array(
          'apachesolr_mlt_delete_block_form',
          arg(5),
        ),
        'access' => user_access('administer search'),
        '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;

    // Set a random 6 digit base-36 number as the hash.
    $hash = substr(base_convert(sha1(uniqid($base_url, TRUE)), 16, 36), 0, 6);
    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']) {
        switch ($GLOBALS['db_type']) {
          case 'mysql':
          case 'mysqli':
            db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid SET asn.changed = %d WHERE n.uid = %d", time(), $account->uid);
            break;
          default:
            db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE uid = %d)", time(), $account->uid);
            break;
        }
      }
      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') {
    switch ($GLOBALS['db_type']) {
      case 'mysql':
      case 'mysqli':
        db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {term_node} tn ON asn.nid = tn.nid SET asn.changed = %d WHERE tn.tid = %d", time(), $edit['tid']);
        break;
      default:
        db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {term_node} WHERE tid = %d)", time(), $edit['tid']);
        break;
    }
  }

  // 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.
    switch ($GLOBALS['db_type']) {
      case 'mysql':
      case 'mysqli':
        db_query("UPDATE {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid SET asn.changed = %d WHERE (n.type = '%s' OR n.type = '%s')", time(), $info->old_type, $info->type);
        break;
      default:
        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);
        break;
    }
  }
}

/**
 * Helper function for modules implementing 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)) {
    switch ($GLOBALS['db_type']) {
      case 'mysql':
      case 'mysqli':
        db_query("DELETE FROM {apachesolr_search_node} USING {apachesolr_search_node} asn INNER JOIN {node} n ON asn.nid = n.nid WHERE n.type = '%s'", $type);
        break;
      default:
        db_query("DELETE FROM {apachesolr_search_node} WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s')", $type);
        break;
    }

    // Populate table
    db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed)\n              SELECT n.nid, n.status, %d AS changed\n              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)\n              SELECT n.nid, n.status, GREATEST(n.created, n.changed, COALESCE(c.last_comment_timestamp, 0)) AS changed\n              FROM {node} n\n              LEFT JOIN {node_comment_statistics} c ON n.nid = c.nid");
    }
    else {
      db_query("INSERT INTO {apachesolr_search_node} (nid, status, changed)\n              SELECT n.nid, n.status, GREATEST(n.created, n.changed) AS changed\n              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', APACHESOLR_READ_WRITE)) {
    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())), 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;

  // Always build the content for the index as an anonynmous user.
  global $user;
  session_save_session(FALSE);
  $saved_user = $user;
  $user = drupal_anonymous_user();
  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', t('Error constructing documents to index: <br /> !message', array(
        '!message' => nl2br(strip_tags($e
          ->getMessage())),
      )), WATCHDOG_ERROR);
      break;
    }
  }

  // Restore the user.
  $user = $saved_user;
  session_save_session(TRUE);
  if (count($documents)) {
    try {
      watchdog('Apache Solr', t('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', t('Indexing failed on one of the following nodes: @nids <br /> !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())), 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', 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())), 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'),
      );
      return $blocks;
    case 'view':

      // From D6: if ($delta != 'sort' && ($node = menu_get_object()) && (!arg(2) || arg(2) == 'view')) {
      if ($delta != 'sort' && (arg(0) == 'node' && is_numeric(arg(1)) && ($node = node_load(arg(1)))) && (!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(
                '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 = $toggle[$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);
        if (!empty($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(
        'delta' => $delta,
      );
      $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 !== FALSE) {
    switch ($gap) {
      case 'YEAR':
        return format_date($unix, 'custom', 'Y', 0);
      case 'MONTH':
        return format_date($unix, 'custom', 'F Y', 0);
      case 'DAY':
        return format_date($unix, 'custom', 'F j, Y', 0);
      case 'HOUR':
        return format_date($unix, 'custom', 'g A', 0);
      case 'MINUTE':
        return format_date($unix, 'custom', 'g:i A', 0);
      case 'SECOND':
        return format_date($unix, 'custom', 'g:i:s A', 0);
    }
  }
  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('Ym', $start) != date('Ym', $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',
  );
  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;
    $solr = new $class($host, $port, $path);

    // Set a non-default behavior.
    $solr
      ->setCollapseSingleValueArrays(FALSE);
    $solr_cache[$host][$port][$path] = $solr;
  }
  return $solr_cache[$host][$port][$path];
}

/**
 * Execute a search based on a query object.
 *
 * Normally this function is used with the default (dismax) handler for keyword
 * searches. The $final_query that's returned will have been modified by
 * both hook_apachesolr_prepare_query() and hook_apachesolr_modify_query().
 *
 * @param $caller
 *   String, name of the calling module or function for use as a cache namespace.
 * @param $current_query
 *   A query object from apachesolr_drupal_query().  It will be modified by
 *   hook_apachesolr_prepare_query() and then cached in apachesolr_current_query().
 * @param $params
 *   Array of parameters to pass to Solr.  Must include at least 'rows'.
 * @param $page
 *   For paging into results, using $params['rows'] results per page.
 *
 * @return array($final_query, $response)
 *
 * @throws Exception
 */
function apachesolr_do_query($caller, $current_query, &$params = array(
  'rows' => 10,
), $page = 0) {

  // Allow modules to alter the query prior to statically caching it.
  // This can e.g. be used to add available sorts.
  foreach (module_implements('apachesolr_prepare_query') as $module) {
    $function_name = $module . '_apachesolr_prepare_query';
    $function_name($current_query, $params, $caller);
  }

  // Cache the original query. Since all the built queries go through
  // this process, all the hook_invocations will happen later
  $query = apachesolr_current_query($current_query, $caller);

  // This hook allows modules to modify the query and params objects.
  apachesolr_modify_query($query, $params, $caller);
  $params['start'] = $page * $params['rows'];
  if (!$query) {
    return array(
      NULL,
      array(),
    );
  }

  // Final chance for the caller to modify the query and params. The signature
  // is: CALLER_finalize_query(&$query, &$params);
  $function = $caller . '_finalize_query';
  if (function_exists($function)) {
    $function($query, $params);
  }
  $keys = $query
    ->get_query_basic();
  if ($keys == '' && isset($params['fq'])) {

    // Move the fq params to q.alt for better performance.
    $qalt = array();
    foreach ($params['fq'] as $delta => $value) {

      // Move the fq param if it has no local params and is not negative.
      if (!preg_match('/^(?:\\{!|-)/', $value)) {
        $qalt[] = $value;
        unset($params['fq'][$delta]);
      }
    }
    if ($qalt) {
      $params['q.alt'] = implode(' ', $qalt);
    }
  }

  // This is the object that does the communication with the solr server.
  $solr = apachesolr_get_solr();

  // We must run htmlspecialchars() here since converted entities are in the index.
  // and thus bare entities &, > or < won't match.
  $response = $solr
    ->search(htmlspecialchars($keys, ENT_NOQUOTES, 'UTF-8'), $params['start'], $params['rows'], $params);

  // The response is cached so that it is accessible to the blocks and anything
  // else that needs it beyond the initial search.
  apachesolr_static_response_cache($response, $caller);
  return array(
    $query,
    $response,
  );
}

/**
 * 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.
 *
 * @todo reverse the order of parameters in future branches.
 */
function apachesolr_static_response_cache($response = NULL, $namespace = 'apachesolr_search') {
  static $_response = array();
  if (is_object($response)) {
    $_response[$namespace] = clone $response;
  }
  if (!isset($_response[$namespace])) {
    $_response[$namespace] = NULL;
  }
  return $_response[$namespace];
}

/**
 * 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())), WATCHDOG_ERROR);
    $query = NULL;
  }
  return $query;
}

/**
 * Static getter/setter for the current query
 *
 * @todo reverse the order of parameters in future branches.
 */
function apachesolr_current_query($query = NULL, $namespace = 'apachesolr_search') {
  static $saved_query = array();
  if (is_object($query)) {
    $saved_query[$namespace] = clone $query;
  }
  return is_object($saved_query[$namespace]) ? clone $saved_query[$namespace] : NULL;
}

/**
 * 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($field = NULL, $reset = FALSE) {
  static $fields;
  if (!isset($fields) || $reset) {
    if (!$reset && ($cache = cache_get('cck_fields', 'cache_apachesolr'))) {
      $fields = unserialize($cache->data);
    }
    else {
      $fields = array();

      // If CCK isn't enabled, do nothing.
      if (module_exists('content')) {

        // A single default mapping for all text fields.
        $mappings['text'] = array(
          'options_select' => array(
            'indexing_callback' => '',
            'display_callback' => 'apachesolr_cck_text_field_callback',
            'index_type' => 'string',
          ),
          'options_buttons' => array(
            'indexing_callback' => '',
            'display_callback' => 'apachesolr_cck_text_field_callback',
            'index_type' => 'string',
          ),
        );
        $mappings['nodereference'] = array(
          'nodereference_select' => array(
            'display_callback' => 'apachesolr_cck_nodereference_field_callback',
            'index_type' => 'integer',
          ),
          'nodereference_autocomplete' => array(
            'display_callback' => 'apachesolr_cck_nodereference_field_callback',
            'index_type' => 'integer',
          ),
        );
        $mappings['userreference'] = array(
          'userreference_select' => array(
            'display_callback' => 'apachesolr_cck_userreference_field_callback',
            'index_type' => 'integer',
          ),
          'userreference_autocomplete' => array(
            'display_callback' => 'apachesolr_cck_userreference_field_callback',
            'index_type' => 'integer',
          ),
        );

        // 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, i.type_name AS content_type FROM {node_field_instance} i INNER JOIN {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($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->indexing_callback = $mappings['per-field'][$row->field_name]['indexing_callback'];
              $row->display_callback = $mappings['per-field'][$row->field_name]['display_callback'];
            }
            else {
              $row->index_type = $mappings[$row->field_type][$row->widget_type]['index_type'];
              $row->indexing_callback = $mappings[$row->field_type][$row->widget_type]['indexing_callback'];
              $row->display_callback = $mappings[$row->field_type][$row->widget_type]['display_callback'];
            }
            $row->multiple = (bool) $row->multiple;

            // It's important that we put the 'cck_' here because several points in the later processing
            // depend on it to route program flow to cck specific handlers.
            $row->name = 'cck_' . $row->field_name;
            $fields[$row->field_name] = array_merge((array) $fields[$row->field_name], (array) $row);
            $fields[$row->field_name]['content types'][] = $row->content_type;
            unset($fields[$row->field_name]['content_type']);
          }
        }
      }
      cache_set('cck_fields', 'cache_apachesolr', serialize($fields));
    }
  }
  return is_null($field) ? $fields : $fields[$field];
}

/**
 * Use the content.module's content_format() to format the
 * field based on its value ($facet).
 *
 *  @param $facet string
 *    The indexed value
 *  @param $options
 *    An array of options including the hook_block $delta.
 */
function apachesolr_cck_text_field_callback($facet, $options) {
  if (function_exists('content_format')) {
    return content_format($options['delta'], array(
      'value' => $facet,
    ));
  }
  else {
    return $facet;
  }
}

/**
 * Use the content.module's content_format() to format the
 * field based on its nid ($facet).
 *
 *  @param $facet string
 *    The indexed value
 *  @param $options
 *    An array of options including the hook_block $delta.
 */
function apachesolr_cck_nodereference_field_callback($facet, $options) {
  if (function_exists('content_format')) {
    return content_format($options['delta'], array(
      'nid' => $facet,
    ), 'plain');
  }
  else {
    return $facet;
  }
}

/**
 * Use the content.module's content_format() to format the
 * field based on its uid ($facet).
 *
 *  @param $facet string
 *    The indexed value
 *  @param $options
 *    An array of options including the hook_block $delta.
 */
function apachesolr_cck_userreference_field_callback($facet, $options) {
  if (function_exists('content_format')) {
    return content_format($options['delta'], array(
      'uid' => $facet,
    ), 'plain');
  }
  else {
    return $facet;
  }
}

/**
 * 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())), WATCHDOG_ERROR);
  }
}

/**
 * Implementation of hook_form_alter
 */
function apachesolr_form_alter($form_id, &$form) {
  switch ($form_id) {

    /**
     * Wiping CCK fields mappings cache. We cannot invoke, as in D6, a hook
     * on fieldapi like we do in D6 with hook_content_fieldapi(), so we add a
     * new submit callback handler to CCK fields forms.
     */
    case '_content_admin_field':
    case '_content_admin_field_remove':
    case '_content_admin_field_add_existing':
    case '_content_admin_field_add_new':
      $form['#submit']['apachesolr_clear_cck_fields_cache'] = array();
      break;
    case 'block_admin_display_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']),
          );
        }
      }
      break;
    case 'block_admin_configure':
      if ($form['module']['#value'] == 'apachesolr' && $form['delta']['#value'] != 'sort') {
        $form['block_settings']['title']['#access'] = FALSE;
      }
      break;
  }
}

/**
 * This function is invoked after a CCK add/edit/delete and is used to clear the cache.
 */
function apachesolr_clear_cck_fields_cache() {
  cache_clear_all('cck_fields', 'cache_apachesolr');
}

/**
 * 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;
  $options_query = drupal_query_string_encode($options['query']);
  $url = $options_query ? url($path, $options_query) : url($path);
  return '<a href="' . check_url($url) . '"' . drupal_attributes($options['attributes']) . '>' . ($options['html'] ? $text : check_plain(html_entity_decode($text))) . '</a>';
}
function theme_apachesolr_unclick_link($facet_text, $path, $options = array()) {
  if (empty($options['html'])) {
    $facet_text = check_plain(html_entity_decode($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');

    // Add translated strings.
    drupal_add_js(array(
      'apachesolr' => array(
        'showMore' => t('Show more'),
        'showFewer' => t('Show fewer'),
      ),
    ), 'setting');

    // 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);

}

/** 
 * Wrapper function for tt() if i18nstrings enabled.
 */
function apachesolr_tt($name, $string, $langcode = NULL, $update = FALSE) {
  if (module_exists('i18nstrings')) {
    return tt($name, $string, $langcode, $update);
  }
  else {
    return $string;
  }
}

Functions

Namesort descending Description
apachesolr_block Implementation of hook_block().
apachesolr_cck_fields Invokes hook_apachesolr_cck_field_mappings to find out how to handle CCK fields.
apachesolr_cck_nodereference_field_callback Use the content.module's content_format() to format the field based on its nid ($facet).
apachesolr_cck_text_field_callback Use the content.module's content_format() to format the field based on its value ($facet).
apachesolr_cck_userreference_field_callback Use the content.module's content_format() to format the field based on its uid ($facet).
apachesolr_clear_cck_fields_cache This function is invoked after a CCK add/edit/delete and is used to clear the cache.
apachesolr_clear_last_index Clear a specific namespace's last changed and nid, or clear all.
apachesolr_comment Implementation of hook_comment().
apachesolr_cron Implementation of hook_cron().
apachesolr_current_query Static getter/setter for the current query
apachesolr_date_determine_gap Determine the best search gap to use for an arbitrary date range.
apachesolr_date_facet_block Helper function for displaying a date facet block.
apachesolr_date_find_query_gap Determine the gap in a date range query filter that we generated.
apachesolr_date_format_iso_by_gap Format an ISO date string based on the gap used to generate it.
apachesolr_date_format_range Format the beginning of a date range query filter that we generated.
apachesolr_date_gap_drilldown Return the next smaller date gap.
apachesolr_date_iso Convert date from timestamp into ISO 8601 format. http://lucene.apache.org/solr/api/org/apache/solr/schema/DateField.html
apachesolr_delete_node_from_index
apachesolr_document_id Generate a unique ID for an entity being indexed.
apachesolr_do_query Execute a search based on a query object.
apachesolr_drupal_query Factory function for query objects.
apachesolr_facetcount_form Used by the 'configure' $op of hook_block so that modules can generically set facet limits on their blocks.
apachesolr_facetcount_save Used by the 'save' $op of hook_block so that modules can generically set facet limits on their blocks.
apachesolr_facet_block Helper function for displaying a facet block.
apachesolr_failure 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.
apachesolr_field_name_map Try to map a schema field name to a human-readable description.
apachesolr_form_alter Implementation of hook_form_alter
apachesolr_get_enabled_facets Return the enabled facets from the specified block array.
apachesolr_get_last_index Returns last changed and last nid for an indexing namespace.
apachesolr_get_nodes_to_index Returns an array of rows from a query based on an indexing namespace.
apachesolr_get_solr 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();
apachesolr_has_searched Semaphore that indicates whether a search has been done. Blocks use this later to decide whether they should load or not.
apachesolr_index_key array('index_type' => 'integer', 'multiple' => TRUE, 'name' => 'fieldname', ),
apachesolr_index_nodes Function to handle the indexing of nodes.
apachesolr_index_status Helper function for modules implementing hook_search's 'status' op.
apachesolr_index_updated Helper function to keep track of when the index has been updated.
apachesolr_l A replacement for l()
apachesolr_mark_node Mark one node as needing re-indexing.
apachesolr_menu Implementation of hook_menu().
apachesolr_mlt_list_blocks Returns a list of blocks. Used by hook_block
apachesolr_mlt_load_block
apachesolr_mlt_suggestions Performs a moreLikeThis query using the settings and retrieves documents.
apachesolr_modify_query This hook allows modules to modify the query and params objects.
apachesolr_nodeapi Implementation of hook_nodeapi().
apachesolr_node_type Implementation of hook_node_type().
apachesolr_pager_init Initialize a pager for theme('pager') without running an SQL query.
apachesolr_rebuild_index_table 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.
apachesolr_save_enabled_facets Save the enabled facets for all modules.
apachesolr_save_module_facets Save the enabled facets for one module.
apachesolr_set_stats_message Call drupal_set_message() with the text.
apachesolr_site_hash Like $site_key in _update_refresh() - returns a site-specific hash.
apachesolr_static_response_cache 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.
apachesolr_taxonomy Implementation of hook_taxonomy().
apachesolr_tt Wrapper function for tt() if i18nstrings enabled.
apachesolr_user Implementation of hook_user().
theme_apachesolr_facet_link
theme_apachesolr_facet_list
theme_apachesolr_mlt_recommendation_block
theme_apachesolr_sort_link
theme_apachesolr_sort_list
theme_apachesolr_unclick_link
_apachesolr_exclude_types
_apachesolr_nodeapi_delete Helper function for hook_nodeapi().
_apachesolr_nodeapi_update Helper function for hook_nodeapi().

Constants

Namesort descending Description
APACHESOLR_READ_ONLY
APACHESOLR_READ_WRITE @file Integration with the Apache Solr search application.

Interfaces

Namesort descending Description
Drupal_Solr_Query_Interface The interface for all 'query' objects.