You are here

apachesolr.module in Apache Solr Search 6.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);

/**
 * Implementation of hook_init().
 *
 * PHP 5.1 compatability code.
 */
function apachesolr_init() {
  if (!function_exists('json_decode')) {

    // Zend files include other files.
    set_include_path(dirname(__FILE__) . PATH_SEPARATOR . get_include_path());
    require_once 'Zend/Json/Decoder.php';
    require_once 'Zend/Json/Encoder.php';

    /**
     * Substitute for missing PHP built-in functions.
     */
    function json_decode($string, $assoc = FALSE) {
      if ($assoc) {
        $objectDecodeType = Zend_Json::TYPE_ARRAY;
      }
      else {
        $objectDecodeType = Zend_Json::TYPE_OBJECT;
      }
      return Zend_Json_Decoder::decode($string, $objectDecodeType);
    }
    function json_encode($data) {
      return Zend_Json_Encoder::encode($data, $objectDecodeType);
    }
  }
}

/**
 * Implementation of hook_menu().
 */
function apachesolr_menu() {
  $items = array();
  $items['admin/settings/apachesolr'] = array(
    'title' => '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/confirm/clear'] = 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/confirm/delete'] = 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/reports/apachesolr/conf'] = array(
    'title' => 'Configuration files',
    'page callback' => 'apachesolr_config_files_overview',
    'access arguments' => array(
      'access site reports',
    ),
    'file' => 'apachesolr.admin.inc',
    'weight' => 5,
    'type' => MENU_LOCAL_TASK,
  );
  $items['admin/reports/apachesolr/conf/%'] = array(
    'title' => 'Configuration file',
    'page callback' => 'apachesolr_config_file',
    'page arguments' => array(
      4,
    ),
    'access arguments' => array(
      'access site reports',
    ),
    'file' => 'apachesolr.admin.inc',
    'type' => MENU_CALLBACK,
  );
  $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;

    // 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') {
    apachesolr_rebuild_index_table($info->type);
  }
  elseif (!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 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)) {
    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
    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");

    // 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();
  }
  cache_clear_all('*', 'cache_apachesolr', TRUE);
}
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.
 *
 * @param string $namespace
 *   Usually the calling module. Is used as a clue for other modules
 *   when they decide whether to create extra $documents, and is used
 *   to track the last_index timestamp.
 * @param $limit
 *   The number of items to return.
 *
 * @return array
 *  An associative array containing:
 *  - nid
 *  - changed
 */
function apachesolr_get_nodes_to_index($namespace, $limit) {
  $rows = array();
  if (variable_get('apachesolr_read_only', APACHESOLR_READ_WRITE) == APACHESOLR_READ_ONLY) {
    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;
}

/**
 * Handles the indexing of nodes.
 *
 * @param array $rows
 *   Each $row in $rows must have:
 *   $row->nid
 *   $row->changed
 * @param string $namespace
 *   Usually the calling module. Is used as a clue for other modules
 *   when they decide whether to create extra $documents, and is used
 *   to track the last_index timestamp.
 *
 * @return timestamp
 *   Either a timestamp representing the last value of apachesolr_get_last_index
 *   to be indexed, or FALSE if indexing failed.
 */
function apachesolr_index_nodes($rows, $namespace) {
  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;
  }
  module_load_include('inc', 'apachesolr', 'apachesolr.index');
  $documents = array();
  $old_position = apachesolr_get_last_index($namespace);
  $position = $old_position;

  // Invoke hook_apachesolr_document_handlers to find out what modules build $documents
  // from nodes in this namespace.
  $callbacks = module_invoke_all('apachesolr_document_handlers', 'node', $namespace);
  $callbacks = array_filter($callbacks, 'function_exists');

  // 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 {

      // Build node. Set reset = TRUE to avoid static caching of all nodes that get indexed.
      if ($node = node_load($row->nid, NULL, TRUE)) {
        foreach ($callbacks as $callback) {

          // The callback can either return a $document or an array of $documents.
          $documents[] = $callback($node, $namespace);
        }
      }

      // Variables to track the last item changed.
      $position['last_change'] = $row->changed;
      $position['last_nid'] = $row->nid;
    } catch (Exception $e) {

      // Something bad happened - log the error.
      watchdog('Apache Solr', 'Error constructing documents to index: <br /> !message', array(
        '!message' => "Node ID: {$row->nid}<br />" . nl2br(strip_tags($e
          ->getMessage())),
      ), WATCHDOG_ERROR);
    }
  }

  // Restore the user.
  $user = $saved_user;
  session_save_session(TRUE);

  // Flatten $documents
  $tmp = array();
  apachesolr_flatten_documents_array($documents, $tmp);
  $documents = $tmp;
  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_set_last_updated($position['last_change']);
    } 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 <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.
 *
 * @param timestamp $date_timestamp
 *   A timestamp to convert to ISO 8601 format
 *
 * @return
 *   A date in ISO 8601 format
 *
 * @see 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 to flatten documents array recursively.
 *
 * @param array $documents
 *   The array of documents being indexed.
 * @param array &$tmp
 *   A container variable that will contain the flattened array.
 */
function apachesolr_flatten_documents_array($documents, &$tmp) {
  foreach ($documents as $index => $item) {
    if (is_array($item)) {
      apachesolr_flatten_documents_array($item, $tmp);
    }
    else {
      $tmp[] = $item;
    }
  }
}
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_set_last_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;
  }
}

/**
 * Set the timestamp of the last index update
 * @param $updated
 *   A timestamp or zero. If zero, the variable is deleted.
 */
function apachesolr_index_set_last_updated($updated = 0) {
  if ($updated) {
    variable_set('apachesolr_index_updated', (int) $updated);
  }
  else {
    variable_del('apachesolr_index_updated');
  }
}

/**
 * Get the timestamp of the last index update.
 * @return integer (timestamp)
 */
function apachesolr_index_get_last_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.
  module_load_include('inc', 'apachesolr', 'apachesolr.index');
  apachesolr_cron_check_node_table();
  try {
    $solr = apachesolr_get_solr();

    // Check for unpublished content that wasn't deleted from the index.
    $result = db_query("SELECT n.nid, n.status FROM {apachesolr_search_node} asn INNER JOIN {node} n ON n.nid = asn.nid WHERE asn.status <> n.status");
    while ($node = db_fetch_object($result)) {
      apachesolr_nodeapi_update($node, FALSE);
    }

    // Check for deleted content that wasn't deleted from the index.
    $result = db_query("SELECT asn.nid FROM {apachesolr_search_node} asn LEFT JOIN {node} n ON n.nid = asn.nid WHERE n.nid IS NULL");
    while ($node = db_fetch_object($result)) {
      apachesolr_nodeapi_delete($node, FALSE);
    }

    // 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_set_last_updated($time);
    }

    // Only clear the cache if the index changed.
    // TODO: clear on some schedule if running multi-site.
    $updated = apachesolr_index_get_last_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_set_last_updated(0);
      }
    }
  } catch (Exception $e) {
    watchdog('Apache Solr', nl2br(check_plain($e
      ->getMessage())) . ' in apachesolr_cron', NULL, WATCHDOG_ERROR);
  }
}

/**
 * Implementation of hook_flush_caches().
 */
function apachesolr_flush_caches() {
  return array(
    'cache_apachesolr',
  );
}

/**
 * A wrapper for cache_clear_all to be used as a submit handler on forms that
 * require clearing Luke cache etc.
 */
function apachesolr_clear_cache() {
  try {
    $solr = apachesolr_get_solr();
    $solr
      ->clearCache();
  } catch (Exception $e) {
    watchdog('Apache Solr', nl2br(check_plain($e
      ->getMessage())), NULL, WATCHDOG_ERROR);
    drupal_set_message(nl2br(check_plain($e
      ->getMessage())), 'warning');
  }
}

/**
 * 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.
 *
 * @see drupal_set_message()
 */
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('Apache Solr', nl2br(check_plain($e
      ->getMessage())), NULL, WATCHDOG_ERROR);
  }
}

/**
 * Return the enabled facets from the specified block array.
 *
 * @param $module
 *   (optional) The name of the module
 *
 * @return array
 *   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 array $enabled
 *   An array consisting of info for all enabled facets.
 *
 * @return array
 *   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 array
 *   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, $delta);
            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;
          if ($name == 'score') {
            $direction = '';
            $new_direction = 'desc';

            // We only sort by descending score.
          }
          elseif ($active) {
            $direction = $toggle[$solrsort['#direction']];
            $new_direction = $toggle[$solrsort['#direction']];
          }
          else {
            $direction = '';
            $new_direction = $sort['default'];
          }
          $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,
          );
          if ($name == 'score') {

            // Set a solrsort parameter so we can distinguish between a user
            // selecting "score desc" and the page defaulting to "score desc".
            $sort_links[$name]['options']['query']['solrsort'] = 'score desc';
          }
        }

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

/**
 * This code makes a decision whether to show a block or not.
 *
 * @param $query
 *   The current query object.
 * @param string $module
 *   The module's name to whom this block belongs.
 * @param string $delta
 *   The delta string the identifies the block within $module.
 *
 * @return boolean
 *   Whether the block should be visible. Other factors, like
 *   the block system's visibility settings, apply as well.
 */
function apachesolr_block_visibility($query, $module, $delta) {

  // TYPE HIERARCHY.
  // If the block is configured to heed type hierarchy then it looks to see if a
  // suitable type filter has been chosen. If not, the function returns.
  // This variable is not static cached because variable_get() already does that.
  $type_filters = variable_get('apachesolr_type_filter', array());
  if (isset($type_filters[$module][$delta]) && $type_filters[$module][$delta] == TRUE) {
    $facet_info = apachesolr_get_facet_definitions();
    if (isset($facet_info[$module][$delta]['content_types'])) {
      $has_filter = $query
        ->get_filters($facet_info[$module][$delta]['facet_field']);
      $show = count($has_filter);
      foreach ($facet_info[$module][$delta]['content_types'] as $content_type) {
        if ($query
          ->has_filter('type', $content_type)) {
          $show = TRUE;
        }
      }
      if (!$show) {
        return FALSE;
      }
    }
  }
  return TRUE;
}
function apachesolr_get_facet_definitions() {
  static $definitions;
  if (!isset($definitions)) {
    $operator_settings = variable_get('apachesolr_operator', array());
    foreach (module_implements('apachesolr_facets') as $module) {
      $facets = module_invoke($module, 'apachesolr_facets');
      if (!empty($facets)) {
        foreach ($facets as $delta => $info) {
          $definitions[$module][$delta] = $info;
          if (isset($definitions[$module][$delta])) {
            $definitions[$module][$delta]['operator'] = isset($operator_settings[$module][$delta]) ? $operator_settings[$module][$delta] : 'AND';
          }
        }
      }
    }
  }
  return $definitions;
}

/**
 * Returns a member of the facet definitions array if it contains
 * $field_name as the 'field_name' element.
 *
 * @param string $field_name
 *   The field_name being sought.
 *
 * @return
 *   A facet definition.
 */
function apachesolr_get_facet_definition_by_field_name($field_name) {
  $definitions = apachesolr_get_facet_definitions();
  foreach ($definitions as $module => $facets) {
    foreach ($facets as $key => $values) {
      if (isset($values['facet_field']) && $values['facet_field'] == $field_name) {
        return $definitions[$module][$key];
      }
    }
  }
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * Make sure to flush cache when content types are changed.
 */
function apachesolr_form_node_type_form_alter(&$form, $form_state) {
  $form['#submit'][] = 'apachesolr_clear_cache';
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * Make sure to flush cache when fields are added.
 */
function apachesolr_form_content_field_overview_form_alter(&$form, $form_state) {
  $form['#submit'][] = 'apachesolr_clear_cache';
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * Make sure to flush cache when fields are updated.
 */
function apachesolr_form_content_field_edit_form_alter(&$form, $form_state) {
  $form['#submit'][] = 'apachesolr_clear_cache';
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * Hide the core 'title' field in favor of our 'name' field.
 * Add a checkbox to enable type-specific visibility.
 */
function apachesolr_form_block_admin_configure_alter(&$form, $form_state) {

  // Hide the core title field.
  if ($form['module']['#value'] == 'apachesolr' && $form['delta']['#value'] != 'sort') {
    $form['block_settings']['title']['#access'] = FALSE;
  }

  // Add a type-specific visibility checkbox.
  $module = $form['module']['#value'];
  $delta = $form['delta']['#value'];
  $enabled_facets = apachesolr_get_enabled_facets();

  // If this block isn't enabled as a facet, get out of here.
  if (!isset($enabled_facets[$module][$delta])) {
    return;
  }
  $facet_info = apachesolr_get_facet_definitions();
  if (isset($facet_info[$module][$delta]['content_types'])) {
    $type_filter_settings = variable_get('apachesolr_type_filter', array());

    // Set up some variables for the verbiage of the form element.
    $count = count($facet_info[$module][$delta]['content_types']);
    $types = format_plural($count, t('type'), t('types'));
    $are = format_plural($count, t('is'), t('are'));
    $content_types = implode(', ', $facet_info[$module][$delta]['content_types']) . '.';
    $form['block_settings']['apachesolr_type_filter'] = array(
      '#type' => 'checkbox',
      '#title' => t('Show this block only when the type filter is selected for: %content_types', array(
        '%content_types' => $content_types,
      )),
      '#default_value' => isset($type_filter_settings[$module][$delta]) ? $type_filter_settings[$module][$delta] : FALSE,
      '#description' => t('This filter is relevant only for specific content types. Check this to display the block only when the type filter has been selected for one of the relevant content types.'),
      '#weight' => 11,
    );
  }
  $operator_settings = variable_get('apachesolr_operator', array());
  $form['block_settings']['apachesolr_operator'] = array(
    '#type' => 'radios',
    '#title' => t('Operator to use for facets'),
    '#options' => drupal_map_assoc(array(
      'AND',
      'OR',
    )),
    '#default_value' => isset($operator_settings[$module][$delta]) ? $operator_settings[$module][$delta] : 'AND',
    '#description' => t('AND filters are exclusive. OR filters are inclusive. Selecting more AND filters narrows the result set. Selecting more OR filters widens the result set.'),
    '#weight' => 12,
  );

  // Add a submit handler to save the value.
  $form['#validate'][] = 'apachesolr_block_admin_configure_submit';
}
function apachesolr_block_admin_configure_submit($form, &$form_state) {
  if (isset($form_state['values']['apachesolr_type_filter'])) {
    $type_filter_settings = variable_get('apachesolr_type_filter', array());
    $module = $form_state['values']['module'];
    $delta = $form_state['values']['delta'];
    unset($type_filter_settings[$module][$delta]);
    $type_filter_settings[$module][$delta] = $form_state['values']['apachesolr_type_filter'];
    variable_set('apachesolr_type_filter', $type_filter_settings);
  }
  if (isset($form_state['values']['apachesolr_operator'])) {
    $operator_settings = variable_get('apachesolr_operator', array());
    $module = $form_state['values']['module'];
    $delta = $form_state['values']['delta'];
    unset($operator_settings[$module][$delta]);
    $operator_settings[$module][$delta] = $form_state['values']['apachesolr_operator'];
    variable_set('apachesolr_operator', $operator_settings);
  }
}

/**
 * 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})) {
    $facet_query_sorts = variable_get('apachesolr_facet_query_sorts', array());
    $contains_active = FALSE;
    $items = array();
    foreach ($response->facet_counts->facet_fields->{$facet_field} as $facet => $count) {
      $options = array(
        'delta' => $delta,
      );
      $exclude = FALSE;

      // Solr sends this back if it's empty.
      if ($facet == '_empty_') {
        $exclude = TRUE;
        $facet = '[* TO *]';
        $options['html'] = TRUE;
      }
      if ($facet_callback && function_exists($facet_callback)) {
        $facet_text = $facet_callback($facet, $options);
      }
      elseif ($exclude) {
        $facet_text = theme('placeholder', t('Missing this field'));
      }
      else {
        $facet_text = $facet;
      }
      $active = $query
        ->has_filter($facet_field, $facet);
      if ($active) {

        // '*' sorts before all numbers.
        $sortpre = '*';
      }
      elseif (isset($facet_query_sorts[$module][$delta]) && strpos($facet_query_sorts[$module][$delta], 'index key') === 0) {

        // If this block is to be alphabetically sorted by key, change $sortpre.
        $sortpre = $facet;
      }
      elseif (isset($facet_query_sorts[$module][$delta]) && strpos($facet_query_sorts[$module][$delta], 'index') === 0) {

        // If this block is to be alphabetically/numerically sorted by value, change $sortpre.
        $sortpre = $facet_text;
      }
      elseif ($exclude) {

        // '-' sorts before all numbers, but after '*'.
        $sortpre = '-';
      }
      else {
        $sortpre = 1000000 - $count;
      }
      $new_query = clone $query;
      if ($active) {
        $contains_active = TRUE;
        $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)) {
      switch ($facet_query_sorts[$module][$delta]) {
        case 'index numeric asc':
          ksort($items, SORT_NUMERIC);
          break;
        case 'index numeric desc':
          krsort($items, SORT_NUMERIC);
          break;
        case 'index desc':
        case 'index key desc':
          krsort($items, SORT_STRING);
          break;
        case 'index asc':
        case 'index key asc':
        default:
          ksort($items, SORT_STRING);
          break;
      }

      // 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, $delta);
      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);
    if (isset($field->start)) {
      $start = $field->start;
      unset($field->start);
    }

    // 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, $delta);
    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';
  }
  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',
    '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());
  $sorts = variable_get('apachesolr_facet_query_sorts', array());
  $children = variable_get('apachesolr_facet_show_children', 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),
  );

  // TODO: Generalize how we know what type a facet block is by putting field
  // type into the facet definition. 'created' and 'changed' are date blocks.
  if ($delta != 'created' && $delta != 'changed') {
    $form['apachesolr_facet_query_sort'] = array(
      '#type' => 'radios',
      '#title' => t('Sort order of facet links'),
      '#options' => array(
        'count' => t('Count'),
        'index asc' => t('Alphabetical, ascending'),
        'index desc' => t('Alphabetical, descending'),
        'index numeric asc' => t('Numeric, ascending'),
        'index numeric desc' => t('Numeric, descending'),
        'index key asc' => t('Key sort, ascending'),
        'index key desc' => t('Key sort, descending'),
      ),
      '#description' => t('The sort order of facet links in this block. %Count, which is the default, will show facets with the most results first. %Alphabetical will sort alphabetically, and %Numeric numerically, either ascending or descending.', array(
        '%Count' => t('Count'),
        '%Alphabetical' => t('Alphanumeric'),
        '%Numeric' => t('Numeric'),
      )),
      '#default_value' => isset($sorts[$module][$delta]) ? $sorts[$module][$delta] : 'count',
    );
  }
  $form['apachesolr_facet_show_children'] = array(
    '#type' => 'radios',
    '#title' => t('Always show child facets'),
    '#options' => array(
      0 => t('No'),
      1 => t('Yes'),
    ),
    '#description' => t('Show the child facets even if the parent facet is not selected.'),
    '#default_value' => isset($children[$module][$delta]) ? $children[$module][$delta] : 0,
  );
  $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);
  $sorts = variable_get('apachesolr_facet_query_sorts', array());
  $sorts[$module][$delta] = $edit['apachesolr_facet_query_sort'];
  variable_set('apachesolr_facet_query_sorts', $sorts);
  $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);
  $children = variable_get('apachesolr_facet_show_children', array());
  $children[$module][$delta] = (int) $edit['apachesolr_facet_show_children'];
  variable_set('apachesolr_facet_show_children', $children);
  $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.
 *
 * @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.
 *
 * @see pager_query()
 */
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, $caller) {
 *   // 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;
  }

  // Call the hooks first because otherwise any modifications to the
  // $query object don't end up in the $params.
  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())) {
    foreach ($fq as $delta => $values) {
      if (is_array($values) || is_object($values)) {
        foreach ($values as $value) {
          $params['fq'][$delta][] = $value;
        }
      }
    }
  }

  // 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 desc') {
      $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();
 *
 * @throws Exception
 */
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];
}

/**
 * Checks if a specific Apache Solr server is available.
 *
 * @return boolean
 *   TRUE if the server can be pinged, FALSE otherwise.
 */
function apachesolr_server_status($host = NULL, $port = NULL, $path = NULL) {
  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');
  }
  $ping = FALSE;
  try {
    $solr = apachesolr_get_solr($host, $port, $path);
    $ping = @$solr
      ->ping(variable_get('apachesolr_ping_timeout', 4));
  } catch (Exception $e) {
    watchdog('Apache Solr', check_plain($e
      ->getMessage()), NULL, WATCHDOG_ERROR);
  }
  return $ping;
}

/**
 * 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. Single quotes are converted
  // too, but not double quotes since the dismax parser looks at them for
  // phrase queries.
  $keys = htmlspecialchars($keys, ENT_NOQUOTES, 'UTF-8');
  $keys = str_replace("'", '&#039;', $keys);
  $response = $solr
    ->search($keys, $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.
 * @param $solr
 *   An instance of Drupal_Apache_Solr_Service.
 *
 * @return
 *   The constructed query
 */
function apachesolr_drupal_query($keys = '', $filters = '', $solrsort = '', $base_path = '', $solr = NULL) {
  list($module, $class) = variable_get('apachesolr_query_class', array(
    'apachesolr',
    'Solr_Base_Query',
  ));
  module_load_include('php', $module, $class);
  if (empty($solr)) {
    $solr = apachesolr_get_solr();
  }
  try {
    $query = new $class($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
 *
 * 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;
}

/**
 * @param array
 *   An array containing:
 *   - index_type: integer
 *   - multiple: boolean
 *   - name: fieldname as string
 */
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) {
  static $map;
  if (!isset($map)) {
    $map = array(
      'body' => t('Body text - the full, rendered content'),
      'title' => t('Title'),
      'teaser' => t('Teaser'),
      'name' => t('Author name'),
      'path_alias' => t('Path alias'),
      'taxonomy_names' => t('All taxonomy term names'),
      'tags_h1' => t('Body text inside H1 tags'),
      'tags_h2_h3' => t('Body text inside H2 or H3 tags'),
      'tags_h4_h5_h6' => t('Body text inside H4, H5, or H6 tags'),
      'tags_inline' => t('Body text in inline tags like EM or STRONG'),
      'tags_a' => t('Body text inside links (A tags)'),
      'tid' => t('Taxonomy term IDs'),
    );
    if (module_exists('taxonomy')) {
      foreach (taxonomy_get_vocabularies() as $vocab) {
        $map['ts_vid_' . $vocab->vid . '_names'] = t('Taxonomy term names only from the %name vocabulary', array(
          '%name' => $vocab->name,
        ));
        $map['im_vid_' . $vocab->vid] = t('Taxonomy term IDs from the %name vocabulary', array(
          '%name' => $vocab->name,
        ));
      }
    }
    foreach (apachesolr_cck_fields() as $name => $field) {
      $map[apachesolr_index_key($field)] = t('CCK @type field %label', array(
        '@type' => $field['index_type'],
        '%label' => $field['label'],
      ));
    }
    drupal_alter('apachesolr_field_name_map', $map);
  }
  return isset($map[$field_name]) ? $map[$field_name] : $field_name;
}

/**
 * Implementation of hook_content_fieldapi().
 */
function apachesolr_content_fieldapi($op, $field) {
  switch ($op) {
    case 'delete instance':
      cache_clear_all('*', 'cache_apachesolr', TRUE);
      apachesolr_mark_node_type($field['type_name']);
      break;
    case 'update instance':
      cache_clear_all('*', 'cache_apachesolr', TRUE);

      // Get the previous value from the table.
      $previous = content_field_instance_read(array(
        'field_name' => $field['field_name'],
        'type_name' => $field['type_name'],
      ));
      $prev_field = array_pop($previous);
      if ($field['display_settings'][NODE_BUILD_SEARCH_INDEX]['exclude'] != $prev_field['display_settings'][NODE_BUILD_SEARCH_INDEX]['exclude']) {
        apachesolr_mark_node_type($field['type_name']);
      }
      elseif ($field['multiple'] != $prev_field['multiple']) {
        apachesolr_mark_node_type($field['type_name']);
      }
      break;
  }
}

/**
 * Mark all nodes of one type as needing re-indexing.
 */
function apachesolr_mark_node_type($type_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.type = '%s'", time(), $type_name);
      break;
    default:
      db_query("UPDATE {apachesolr_search_node} SET changed = %d WHERE nid IN (SELECT nid FROM {node} WHERE type = '%s')", time(), $type_name);
      break;
  }
}

/**
 * Invokes hook_apachesolr_cck_field_mappings to find out how to handle CCK fields.
 */
function apachesolr_cck_fields() {
  static $fields;
  if (!isset($fields)) {
    $fields = array();

    // If CCK isn't enabled, do nothing.
    if (module_exists('content')) {
      module_load_include('inc', 'content', 'includes/content.crud');
      $cck_field_instances = content_field_instance_read();

      // A single default mapping for all text fields.
      $mappings['text'] = array(
        'optionwidgets_select' => array(
          'display_callback' => 'apachesolr_cck_text_field_callback',
          'indexing_callback' => 'apachesolr_cck_text_indexing_callback',
          'index_type' => 'string',
          'facets' => TRUE,
        ),
        'optionwidgets_buttons' => array(
          'display_callback' => 'apachesolr_cck_text_field_callback',
          'indexing_callback' => 'apachesolr_cck_text_indexing_callback',
          'index_type' => 'string',
          'facets' => TRUE,
        ),
        'optionwidgets_onoff' => array(
          'display_callback' => 'apachesolr_cck_text_field_callback',
          'indexing_callback' => 'apachesolr_cck_text_indexing_callback',
          'index_type' => 'string',
          'facets' => TRUE,
        ),
      );

      // A single default mapping for all integer fields using optionwidgets.
      $mappings['number_integer'] = array(
        'optionwidgets_select' => array(
          'display_callback' => 'apachesolr_cck_text_field_callback',
          'indexing_callback' => 'apachesolr_cck_integer_indexing_callback',
          'index_type' => 'sint',
          'facets' => TRUE,
        ),
        'optionwidgets_buttons' => array(
          'display_callback' => 'apachesolr_cck_text_field_callback',
          'indexing_callback' => 'apachesolr_cck_integer_indexing_callback',
          'index_type' => 'sint',
          'facets' => TRUE,
        ),
        'optionwidgets_onoff' => array(
          'display_callback' => 'apachesolr_cck_text_field_callback',
          'indexing_callback' => 'apachesolr_cck_integer_indexing_callback',
          'index_type' => 'sint',
          'facets' => TRUE,
        ),
      );
      $mappings['nodereference'] = array(
        'nodereference_buttons' => array(
          'display_callback' => 'apachesolr_cck_nodereference_field_callback',
          'indexing_callback' => 'apachesolr_cck_nodereference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
        'nodereference_select' => array(
          'display_callback' => 'apachesolr_cck_nodereference_field_callback',
          'indexing_callback' => 'apachesolr_cck_nodereference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
        'nodereference_autocomplete' => array(
          'display_callback' => 'apachesolr_cck_nodereference_field_callback',
          'indexing_callback' => 'apachesolr_cck_nodereference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
      );
      $mappings['userreference'] = array(
        'userreference_buttons' => array(
          'display_callback' => 'apachesolr_cck_userreference_field_callback',
          'indexing_callback' => 'apachesolr_cck_userreference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
        'userreference_select' => array(
          'display_callback' => 'apachesolr_cck_userreference_field_callback',
          'indexing_callback' => 'apachesolr_cck_userreference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
        'userreference_autocomplete' => array(
          'display_callback' => 'apachesolr_cck_userreference_field_callback',
          'indexing_callback' => 'apachesolr_cck_userreference_indexing_callback',
          'index_type' => 'integer',
          'facets' => TRUE,
        ),
      );

      // Allow other modules to add or alter mappings.
      drupal_alter('apachesolr_cck_fields', $mappings);
      foreach ($cck_field_instances as $instance) {
        $field_type = $instance['type'];
        $field_name = $instance['field_name'];
        $widget_type = $instance['widget']['type'];
        if (!isset($mappings[$field_type][$widget_type]) && !isset($mappings['per-field'][$field_name]) && isset($mappings[$field_type]['default'])) {
          $widget_type = 'default';
        }

        // Only deal with fields that have index mappings and that have not been marked for exclusion.
        if ((isset($mappings[$field_type][$widget_type]) || isset($mappings['per-field'][$field_name])) && empty($instance['display_settings'][NODE_BUILD_SEARCH_INDEX]['exclude'])) {
          if (isset($mappings['per-field'][$field_name])) {
            $instance['index_type'] = $mappings['per-field'][$field_name]['index_type'];
            $instance['indexing_callback'] = $mappings['per-field'][$field_name]['indexing_callback'];
            $instance['display_callback'] = $mappings['per-field'][$field_name]['display_callback'];
            if (isset($mappings['per-field'][$field_name]['facet_block_callback'])) {
              $instance['facet_block_callback'] = $mappings['per-field'][$field_name]['facet_block_callback'];
            }
            $instance['facets'] = $mappings['per-field'][$field_name]['facets'];
          }
          else {
            $instance['index_type'] = $mappings[$field_type][$widget_type]['index_type'];
            $instance['indexing_callback'] = $mappings[$field_type][$widget_type]['indexing_callback'];
            $instance['display_callback'] = $mappings[$field_type][$widget_type]['display_callback'];
            if (isset($mappings[$field_type][$widget_type]['facet_block_callback'])) {
              $instance['facet_block_callback'] = $mappings[$field_type][$widget_type]['facet_block_callback'];
            }
            $instance['facets'] = $mappings[$field_type][$widget_type]['facets'];
          }
          $instance['multiple'] = (bool) $instance['multiple'];
          $instance['name'] = 'cck_' . $field_name;
          if (isset($fields[$field_name]) && is_array($fields[$field_name])) {

            // Merge together settings when used for multiple node types.
            $fields[$field_name] = array_merge($fields[$field_name], $instance);
          }
          else {
            $fields[$field_name] = $instance;
          }
          $fields[$field_name]['content_types'][] = $instance['type_name'];
          unset($fields[$field_name]['type_name']);
        }
      }
    }
  }
  return $fields;
}

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

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

/**
 * 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' => 0,
        'delta' => '',
      ),
    ),
    // 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,
        'delta' => NULL,
      ),
    ),
  );
}

/**
 * Performs a moreLikeThis query using the settings and retrieves documents.
 *
 * @param array $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);
    $type_filters = array();
    if (is_array($settings['mlt_type_filters'])) {
      foreach ($settings['mlt_type_filters'] as $type_filter) {
        $type_filters[] = "type:{$type_filter}";
      }
      $params['fq']['mlt'][] = '(' . implode(' OR ', $type_filters) . ') ';
    }
    if ($custom_filters = $settings['mlt_custom_filters']) {
      $params['fq']['mlt'][] = $custom_filters;
    }

    // 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']),
      );
    }
  }
}

/**
 * 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, $delta) {
  $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, NULL, 'ul', array(
    'class' => 'apachesolr-mlt-block',
  ));
}
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
 *
 * @return
 *   An HTML string containing a link to the given path.
 *
 * @see http://api.drupal.org/api/function/l/6
 */
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 '<a href="' . check_url(url($path, $options)) . '"' . drupal_attributes($options['attributes']) . '>' . ($options['html'] ? $text : check_plain(html_entity_decode($text))) . '</a>';
}
function apachesolr_js() {
  static $settings;

  // Only add the js stuff once.
  if (is_null($settings)) {
    $settings['apachesolr_facetstyle'] = variable_get('apachesolr_facetstyle', 'checkboxes');

    // This code looks for enabled facet blocks and injects the block #ids into
    // Drupal.settings as jQuery selectors to add the Show more links.
    $show_more_blocks = array();
    $facet_map = array();
    foreach (apachesolr_get_facet_definitions() as $module => $definitions) {
      foreach ($definitions as $facet => $facet_definition) {
        $facet_map[$facet_definition['facet_field']] = $facet;
      }
    }
    $show_more_selector = array();
    foreach (apachesolr_get_enabled_facets() as $module => $blocks) {
      foreach ($blocks as $block) {
        $show_more_selector[] = "#block-{$module}-{$facet_map[$block]}:has(.apachesolr-hidden-facet) .apachesolr-facet-list";
      }
    }
    $settings['apachesolr_show_more_blocks'] = implode(', ', $show_more_selector);
    drupal_add_js($settings, 'setting');
    drupal_add_js(drupal_get_path('module', 'apachesolr') . '/apachesolr.js');
  }
}
function theme_apachesolr_unclick_link($facet_text, $path, $options = array()) {
  apachesolr_js();
  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, $delta = '') {
  apachesolr_js();

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

    // 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,
        );
      }
      $hidden_item['class'] = isset($hidden_item['class']) ? $hidden_item['class'] . ' apachesolr-hidden-facet' : 'apachesolr-hidden-facet';
      $items[] = $hidden_item;
    }
  }
  $admin_link = '';
  if (user_access('administer search')) {
    $admin_link = l(t('Configure enabled filters'), 'admin/settings/apachesolr/enabled-filters');
  }
  return theme('item_list', $items, NULL, 'ul', array(
    'class' => 'apachesolr-facet-list',
  )) . $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, NULL, 'ul', array(
    'class' => 'apachesolr-sort-list',
  ));
}

/**
 * The interface for all 'query' objects.
 */
interface Drupal_Solr_Query_Interface {

  /**
   * Get all filters, or the subset of filters for one field.
   *
   * @param $name
   *   (optional) Name of a Solr field. Defaults to NULL.
   */
  function get_filters($name = NULL);

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

  /**
   * 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) If TRUE, the filter will be negative,
   *   meaning that matching values will be excluded from the
   *   result set. Defaults to FALSE.
   */
  function add_filter($field, $value, $exclude = FALSE);

  /**
   * Remove a filter from the query.
   *
   * @param string $field
   *   The facet field to remove
   * @param string $value
   *   (optional) The facet value to remove. Defaults to NULL.
   */
  function remove_filter($field, $value = NULL);

  /**
   * Get the query's keywords.
   */
  function get_keys();

  /**
   * Set the query's keywords.
   *
   * @param string $keys
   *   The new keywords.
   */
  function set_keys($keys);

  /**
   * Removes the query's keywords.
   */
  function remove_keys();

  /**
   * Return the search path (including the search keywords).
   */
  function get_path();

  /**
   * Return filters and sort in a form suitable for a query param to url().
   */
  function get_url_queryvalues();

  /**
   * Return the basic string query.
   */
  function get_query_basic();

  /**
   * Return the sorts that are provided by the query object.
   *
   * @return array all the sorts provided
   */
  function get_available_sorts();

  /**
   * Make a sort available.
   */
  function set_available_sort($field, $sort);

  /**
   * Get the solrsort.
   *
   * Returns the non-urlencode, non-aliased sort field and direction.
   * as an array keyed with '#name' and '#direction'.
   */
  function get_solrsort();

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

  /**
   * 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
   *   (optional) The operator to use within the filter part of the subquery.
   *   Defaults to 'OR'
   * @param string $q_operator
   *   (optional) 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. Defaults to 'AND'
   */
  function add_subquery(Drupal_Solr_Query_Interface $query, $fq_operator = 'OR', $q_operator = 'AND');

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

}

/**
 * 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_block_admin_configure_submit
apachesolr_block_visibility This code makes a decision whether to show a block or not.
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_cache A wrapper for cache_clear_all to be used as a submit handler on forms that require clearing Luke cache etc.
apachesolr_clear_last_index Clear a specific namespace's last changed and nid, or clear all.
apachesolr_comment Implementation of hook_comment().
apachesolr_content_fieldapi Implementation of hook_content_fieldapi().
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.
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_exclude_types
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_flatten_documents_array Function to flatten documents array recursively.
apachesolr_flush_caches Implementation of hook_flush_caches().
apachesolr_form_block_admin_configure_alter Implementation of hook_form_[form_id]_alter().
apachesolr_form_block_admin_display_form_alter Implementation of hook_form_[form_id]_alter
apachesolr_form_content_field_edit_form_alter Implementation of hook_form_[form_id]_alter().
apachesolr_form_content_field_overview_form_alter Implementation of hook_form_[form_id]_alter().
apachesolr_form_node_type_form_alter Implementation of hook_form_[form_id]_alter().
apachesolr_get_enabled_facets Return the enabled facets from the specified block array.
apachesolr_get_facet_definitions
apachesolr_get_facet_definition_by_field_name Returns a member of the facet definitions array if it contains $field_name as the 'field_name' element.
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_get_last_updated Get the timestamp of the last index update.
apachesolr_index_key
apachesolr_index_nodes Handles the indexing of nodes.
apachesolr_index_set_last_updated Set the timestamp of the last index update
apachesolr_index_status Helper function for modules implmenting hook_search's 'status' op.
apachesolr_init Implementation of hook_init().
apachesolr_js
apachesolr_l A replacement for l()
apachesolr_mark_node Mark one node as needing re-indexing.
apachesolr_mark_node_type Mark all nodes of one type 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_nodeapi_delete Helper function for hook_nodeapi().
apachesolr_nodeapi_update Helper function for 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_server_status Checks if a specific Apache Solr server is available.
apachesolr_set_stats_message Call drupal_set_message() with the text. The text is translated with t() and substituted using Solr stats.
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_theme Implementation of hook_theme().
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

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.