You are here

apachesolr_search.module in Apache Solr Search 6.2

Provides a content search implementation for node content for use with the Apache Solr search application.

File

apachesolr_search.module
View source
<?php

/**
 * @file
 *   Provides a content search implementation for node content for use with the
 *   Apache Solr search application.
 */

/**
 * Implementation of hook_help().
 */
function apachesolr_search_help($section) {
  switch ($section) {
    case 'admin/settings/apachesolr/index':
      if (variable_get('apachesolr_read_only', APACHESOLR_READ_WRITE) == APACHESOLR_READ_ONLY) {
        return t('Operating in read-only mode; updates are disabled.');
      }
      $remaining = 0;
      $total = 0;

      // Collect the stats
      $status = apachesolr_index_status('apachesolr_search');
      $remaining += $status['remaining'];
      $total += $status['total'];
      return t('The search index is generated by !cron. %percentage of the site content has been sent to the server. There @items left to send.', array(
        '!cron' => l(t('running cron'), 'admin/reports/status/run-cron', array(
          'query' => array(
            'destination' => 'admin/settings/apachesolr/index',
          ),
        )),
        '%percentage' => (int) min(100, 100 * ($total - $remaining) / max(1, $total)) . '%',
        '@items' => format_plural($remaining, t('is 1 item'), t('are @count items')),
      ));
  }
}

/**
 * Implementation of hook_menu().
 */
function apachesolr_search_menu() {
  $items['admin/settings/apachesolr/query-fields'] = array(
    'title' => 'Search fields',
    'page callback' => 'apachesolr_search_settings_page',
    'access arguments' => array(
      'administer search',
    ),
    'weight' => 1,
    'type' => MENU_LOCAL_TASK,
    'file' => 'apachesolr_search.admin.inc',
  );
  $items['admin/settings/apachesolr/content-bias'] = array(
    'title' => 'Content bias settings',
    'page callback' => 'apachesolr_boost_settings_page',
    'access arguments' => array(
      'administer search',
    ),
    'weight' => 1,
    'type' => MENU_LOCAL_TASK,
    'file' => 'apachesolr_search.admin.inc',
  );
  return $items;
}

/**
 * Implementation of hook_menu_alter().
 */
function apachesolr_search_menu_alter(&$menu) {
  if (isset($menu['search/apachesolr_search/%menu_tail'])) {
    $menu['search']['page callback'] = 'apachesolr_search_view';
    $menu['search/apachesolr_search/%menu_tail']['page callback'] = 'apachesolr_search_view';
  }
  if (variable_get('apachesolr_search_make_default', 0)) {
    if (isset($menu['search/node/%menu_tail'])) {

      // Hide the node search tab.
      $menu['search/node/%menu_tail']['type'] = MENU_CALLBACK;
      unset($menu['search/node/%menu_tail']['title callback'], $menu['search/node/%menu_tail']['title arguments']);
      $menu['search/node/%menu_tail']['title'] = 'Search';
    }
    if (isset($menu['search/apachesolr_search/%menu_tail'])) {

      // Alter the solr search tab
      $menu['search/apachesolr_search/%menu_tail']['weight'] = -10;
      unset($menu['search/apachesolr_search/%menu_tail']['title callback'], $menu['search/apachesolr_search/%menu_tail']['title arguments']);
      $menu['search/apachesolr_search/%menu_tail']['title'] = 'Content';
    }
  }
  if (variable_get('apachesolr_search_taxonomy_links', 0)) {
    if (isset($menu['taxonomy/term/%'])) {
      $menu['taxonomy/term/%']['module'] = 'apachesolr_search';
      $menu['taxonomy/term/%']['page callback'] = 'apachesolr_search_taxonomy_term_page';
      $menu['taxonomy/term/%']['file'] = 'apachesolr.taxonomy.inc';
    }
  }
}

/**
 * Implementation of hook_cron(). Indexes nodes.
 */
function apachesolr_search_cron() {
  $cron_limit = variable_get('apachesolr_cron_limit', 50);
  $rows = apachesolr_get_nodes_to_index('apachesolr_search', $cron_limit);
  apachesolr_index_nodes($rows, 'apachesolr_search');
}

/**
 * Implementation of hook_apachesolr_types_exclude().
 */
function apachesolr_search_apachesolr_types_exclude($namespace) {
  if ($namespace == 'apachesolr_search') {
    $excluded_types = variable_get('apachesolr_search_excluded_types', array());
    return array_filter($excluded_types);
  }
}

/**
 * Implementation of hook_search().
 */
function apachesolr_search_search($op = 'search', $keys = NULL) {
  switch ($op) {
    case 'name':
      return t('Search');
    case 'reset':
      apachesolr_clear_last_index('apachesolr_search');
      return;
    case 'status':
      return apachesolr_index_status('apachesolr_search');
    case 'search':
      $filters = isset($_GET['filters']) ? $_GET['filters'] : '';
      $solrsort = isset($_GET['solrsort']) ? $_GET['solrsort'] : '';
      $page = isset($_GET['page']) ? $_GET['page'] : 0;
      try {
        $results = apachesolr_search_execute($keys, $filters, $solrsort, 'search/' . arg(1), $page);
        return $results;
      } catch (Exception $e) {
        watchdog('Apache Solr', nl2br(check_plain($e
          ->getMessage())), NULL, WATCHDOG_ERROR);
        apachesolr_failure(t('Solr search'), $keys);
      }
      break;
  }

  // switch
}

/**
 * Re-implementation of search_view().
 */
function apachesolr_search_view($type = NULL) {
  if (empty($type)) {

    // Note: search/X can not be a default tab because it would take on the
    // path of its parent (search). It would prevent remembering keywords when
    // switching tabs. This is why we drupal_goto to it from the parent instead.
    drupal_goto('search/apachesolr_search');
  }
  $keys = trim(search_get_keys());

  // Construct the search form.  If the user submits POST data, this will
  // redirect to a GET request before the search actually runs.
  if (isset($_POST['form_id']) && $_POST['form_id'] == 'search_form') {
    $form = drupal_get_form('search_form', NULL, $keys, $type);
  }

  // We did not redirect, so run the search if needed.
  $content = '';
  $filters = '';
  if (isset($_GET['filters'])) {
    $filters = trim($_GET['filters']);
  }

  // Only perform search if there is non-whitespace search term or filters:
  if ($keys || $filters || variable_get('apachesolr_search_browse', 'browse') == 'results') {
    if (variable_get('apachesolr_logging', TRUE)) {

      // Log the search keys:
      $log = $keys;
      if ($filters) {
        $log .= 'filters=' . $filters;
      }
      watchdog('search', '%keys (@type).', array(
        '%keys' => $log,
        '@type' => t('Search'),
      ), WATCHDOG_NOTICE, l(t('results'), 'search/' . $type . '/' . $keys));
    }

    // Collect the search results. See search_data().
    $content = module_invoke($type, 'search', 'search', $keys);
    if (isset($content) && is_array($content) && count($content)) {
      if (module_hook($type, 'search_page')) {
        $output = module_invoke($type, 'search_page', $content);
      }
      else {
        $output = theme('search_results', $content, $type);
      }
      return drupal_get_form('search_form', NULL, $keys, $type) . $output;
    }

    // See search_view().
    if ($content) {
      $content = theme('box', t('Search results'), $content);
    }
    else {
      $content = theme('box', t('Your search yielded no results'), theme('apachesolr_search_noresults'));
    }
  }
  elseif ($type != 'node') {

    // Ignore $type == node. Since we override the menu path to search to point
    // to this function, we have to count on core searches coming in here, too.
    switch (variable_get('apachesolr_search_browse', 'browse')) {
      case 'browse':
        try {

          // Show search form and browse-by blocks.
          $blocks = apachesolr_search_browse('', '', '', 'search/' . $type);
          if (count($blocks) > 0) {
            $content = theme('apachesolr_browse_blocks', $blocks);
          }
        } catch (Exception $e) {
          watchdog('Apache Solr', nl2br(check_plain($e
            ->getMessage())), NULL, WATCHDOG_ERROR);
          apachesolr_failure(t('Solr search'), $keys);
        }
        break;
      case 'blocks':

        // Launch search so that hook_block() will show blocks.
        $dummy = search_data($keys, $type);
        break;
      default:
        break;
    }
  }
  if (empty($form)) {

    // The form may be altered based on the query that was run.
    $form = drupal_get_form('search_form', NULL, $keys, $type);
  }
  return theme('apachesolr_search_results_page', $form, $content);
}

/**
 * Implementation of hook_apachesolr_document_handlers().
 *
 * @param string $type
 *   Entity type. 'node', 'comment', and so forth. Used to evaluate whether this module
 *   should be interested in creating documents.
 * @param string $namespace
 *   Usually the name of the module that is initiating indexing. In this case
 *   we want to register a handler if the namespace is 'apachesolr_search'.
 * @return array $handlers
 *   An array of strings that are function names. Each function returns a $document from
 *   an entity (of type $type).
 */
function apachesolr_search_apachesolr_document_handlers($type, $namespace) {
  if ($type == 'node' && $namespace == 'apachesolr_search') {
    return array(
      'apachesolr_node_to_document',
    );
  }
}

/**
 * Implementation of hook_finalize_query().
 *
 * Handle OR facets.
 */
function apachesolr_search_finalize_query(&$query, &$params) {
  $ors = array();
  $facet_info = apachesolr_get_facet_definitions();
  foreach ($facet_info as $infos) {
    foreach ($infos as $delta => $facet) {
      if ($facet['operator'] == 'OR') {
        $ors[] = $delta;
      }
    }
  }
  if (isset($params['fq'])) {
    $filter_queries = $params['fq'];
    foreach ($filter_queries as $delta => $values) {
      $fq = $tag = '';
      $op = 'AND';
      $fields = array(
        $delta,
      );

      // CCK and taxonomy facet field block deltas are not the same as their Solr index field names.
      if (strpos($delta, '_cck_') !== FALSE) {
        $fields[] = trim(drupal_substr($delta, 7, drupal_strlen($delta)));
      }
      elseif ($delta == 'tid') {
        if (function_exists('taxonomy_get_term')) {
          foreach ($values as $value) {
            $filters = $query
              ->filter_extract($value, 'tid');
            $term = taxonomy_get_term($filters[0]['#value']);
            $fields[] = 'im_vid_' . $term->vid;
          }
        }
      }
      if (array_intersect($fields, $ors)) {
        $tag = "{!tag={$delta}}";
        $op = 'OR';
      }
      $fq = implode(" {$op} ", $params['fq'][$delta]);
      $params['fq'][] = $tag . $fq;
      unset($params['fq'][$delta]);
    }
  }
}

/**
 * Execute an empty search (match all documents) and show a listing of all enabled facets.
 */
function apachesolr_search_browse($keys = '', $filters = '', $solrsort = '', $base_path = '') {
  global $user, $theme_key;
  $query = apachesolr_drupal_query($keys, $filters, $solrsort, $base_path);
  $params = array(
    'start' => 0,
    'rows' => 0,
    'facet' => 'true',
    'facet.mincount' => 1,
    'facet.sort' => 'true',
  );
  apachesolr_search_add_facet_params($params, $query);

  // 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($query, $params, 'apachesolr');
  }
  $solr = apachesolr_get_solr();
  apachesolr_current_query($query);
  apachesolr_modify_query($query, $params, 'apachesolr');
  $response = $solr
    ->search('', $params['start'], $params['rows'], $params);
  if (empty($response)) {
    return;
  }
  apachesolr_static_response_cache($response);
  apachesolr_has_searched(TRUE);

  // Get blocks for all enabled filters
  $blocks = array();
  $rids = array_keys($user->roles);
  foreach (apachesolr_get_enabled_facets() as $module => $module_facets) {
    if (!module_exists($module)) {

      // When modules are disabled their facet settings may remain.
      continue;
    }
    foreach ($module_facets as $delta => $facet_field) {
      if ($delta == 'currentsearch') {
        continue;
      }
      if (count((array) $response->facet_counts->facet_fields->{$facet_field}) > 0 || count((array) $response->facet_counts->facet_dates->{$facet_field}) > 0) {

        // This bit is modeled on block.module, block_list().
        $hook_block = (object) module_invoke($module, 'block', 'view', $delta);
        $result = db_query_range(db_rewrite_sql("SELECT DISTINCT b.* FROM {blocks} b\n          LEFT JOIN {blocks_roles} r ON b.module = r.module AND b.delta = r.delta\n          WHERE b.module = '%s' AND b.delta = '%s' AND b.theme = '%s' AND b.status = 1\n            AND (r.rid IN (" . db_placeholders($rids) . ") OR r.rid IS NULL)", 'b', 'bid'), array_merge(array(
          $module,
          $delta,
          $theme_key,
        ), $rids), 0, 1);
        $block = db_fetch_object($result);
        $block->content = $hook_block->content;
        $block->subject = str_replace(t('Filter by '), t('Browse by '), $hook_block->subject);

        // We can safely assume these values, since we're taking over the block display.
        $block->visibility = TRUE;
        $block->enabled = TRUE;
        $block->module = $module;

        // This hook is made up. It should be in Drupal core, and it is quite useful.
        // Including it here because it has saved the day on projects.
        drupal_alter('block', $block);
        if ($block->enabled && $block->visibility) {
          $blocks["{$module}_{$delta}"] = $block;
        }
      }
    }
  }
  apachesolr_has_searched(FALSE);
  usort($blocks, create_function('$a, $b', 'return $a->weight - $b->weight;'));
  return $blocks;
}

/**
 * Theming function that shows a groups of blocks so users can start a search from a filter.
 *
 * @param $blocks an array of block objects.
 */
function theme_apachesolr_browse_blocks($blocks) {
  $result = "<div class='apachesolr_browse_block'><h2>" . t('Browse available categories') . '</h2>';
  $result .= '<p>' . t('Pick a category to launch a search.') . '</p>';
  foreach ($blocks as $facet_field => $block) {
    $result .= theme('block', $block);
  }
  $result .= '</div>';
  return $result;
}

/**
 * Execute a search results based on keyword, filter, and sort strings.
 *
 * @param $keys
 * @param $filterstring
 * @param $solrsort
 * @param $base_path
 *   For constructing filter and sort links. Leave empty unless the links need to point somewhere
 *   other than the base path of the current request.
 * @param integer $page
 *   For pagination.
 * @param $caller
 * @return Apache_Solr_Response $response
 * @throws Exception
 */
function apachesolr_search_execute($keys, $filterstring, $solrsort, $base_path = '', $page = 0, $caller = 'apachesolr_search') {
  $params = array();

  // This is the object that knows about the query coming from the user.
  $query = apachesolr_drupal_query($keys, $filterstring, $solrsort, $base_path);
  if (empty($query)) {
    throw new Exception(t('Could not construct a Solr query in function apachesolr_search_search()'));
  }
  $params += apachesolr_search_basic_params($query);
  if ($keys) {
    $params += apachesolr_search_highlighting_params($query);
    $params += apachesolr_search_spellcheck_params($query);
  }
  else {

    // No highlighting, use the teaser as a snippet.
    $params['fl'] .= ',teaser';
  }
  if (module_exists('upload')) {
    $params['fl'] .= ',is_upload_count';
  }
  apachesolr_search_add_facet_params($params, $query);
  apachesolr_search_add_boost_params($params, $query, apachesolr_get_solr());
  list($final_query, $response) = apachesolr_do_query($caller, $query, $params, $page);
  apachesolr_has_searched(TRUE);

  // Add search terms and filters onto the breadcrumb.
  // We use the original $query to avoid exposing, for example, nodeaccess
  // filters in the breadcrumb.
  drupal_set_breadcrumb(array_merge(drupal_get_breadcrumb(), $query
    ->get_breadcrumb()));
  return apachesolr_search_process_response($response, $final_query, $params);
}

// NOTE: Why does this take the $query object?
function apachesolr_search_basic_params($query) {
  $params = array(
    'fl' => 'id,nid,title,comment_count,type,created,changed,score,path,url,uid,name',
    'rows' => variable_get('apachesolr_rows', 10),
    'facet' => 'true',
    'facet.mincount' => 1,
    'facet.sort' => 'true',
  );
  return $params;
}

/**
 * Add highlighting settings to the search params.
 *
 * These settings are set in solrconfig.xml.
 * See the defaults there.
 * If you wish to override them, you can via settings.php
 */
function apachesolr_search_highlighting_params($query) {
  $params['hl'] = variable_get('apachesolr_hl_active', NULL);
  $params['hl.fragsize'] = variable_get('apachesolr_hl_textsnippetlength', NULL);
  $params['hl.simple.pre'] = variable_get('apachesolr_hl_pretag', NULL);
  $params['hl.simple.post'] = variable_get('apachesolr_hl_posttag', NULL);
  $params['hl.snippets'] = variable_get('apachesolr_hl_numsnippets', NULL);
  $params['hl.fl'] = variable_get('apachesolr_hl_fieldtohighlight', NULL);
  return $params;
}
function apachesolr_search_spellcheck_params($query) {
  $params = array();
  if (variable_get('apachesolr_search_spellcheck', TRUE)) {

    //Add new parameter to the search request
    $params['spellcheck.q'] = $query
      ->get_query_basic();
    $params['spellcheck'] = 'true';
  }
  return $params;
}
function apachesolr_search_add_facet_params(&$params, $query) {
  $facet_query_limits = variable_get('apachesolr_facet_query_limits', array());
  $facet_missing = variable_get('apachesolr_facet_missing', array());
  $facet_info = apachesolr_get_facet_definitions();
  foreach (apachesolr_get_enabled_facets() as $module => $module_facets) {
    foreach ($module_facets as $delta => $facet_field) {

      // Checking the block visibility saves us from adding facet fields to the query
      // which won't be used.
      if (apachesolr_block_visibility($query, $module, $delta)) {

        // TODO: generalize handling of date and range facets.
        // TODO: put field type in the facet definitions.
        // NOTE: This is a dependency on the apachesolr_date module. Using function_exists to avoid problems.
        if (isset($facet_info[$module][$delta]['field_type']) && in_array($facet_info[$module][$delta]['field_type'], array(
          'date',
          'datetime',
          'datestamp',
        )) && function_exists('apachesolr_date_search_date_range')) {
          list($start, $end, $gap) = apachesolr_date_search_date_range($query, $facet_field);
          if ($gap) {
            $params['facet.date'][] = $facet_field;
            $params['f.' . $facet_field . '.facet.date.start'] = $start;
            $params['f.' . $facet_field . '.facet.date.end'] = $end;
            $params['f.' . $facet_field . '.facet.date.gap'] = $gap;
          }
        }
        elseif ($module == 'apachesolr_search' && ($facet_field == 'created' || $facet_field == 'changed')) {
          list($start, $end, $gap) = apachesolr_search_date_range($query, $facet_field);
          if ($gap) {
            $params['facet.date'][] = $facet_field;
            $params['f.' . $facet_field . '.facet.date.start'] = $start;
            $params['f.' . $facet_field . '.facet.date.end'] = $end;
            $params['f.' . $facet_field . '.facet.date.gap'] = $gap;
          }
        }
        else {
          $ex = '';
          if ($facet_info[$module][$delta]['operator'] == 'OR') {
            $ex = "{!ex={$facet_field}}";
          }
          $params['facet.field'][] = $ex . $facet_field;

          // Facet limits
          if (isset($facet_query_limits[$module][$delta])) {
            $params['f.' . $facet_field . '.facet.limit'] = $facet_query_limits[$module][$delta];
          }

          // Facet missing
          if (!empty($facet_missing[$module][$delta])) {
            $params['f.' . $facet_field . '.facet.missing'] = 'true';
          }
        }
      }
    }
  }
  if (!empty($params['facet.field'])) {

    // Add a default limit for fields where no limit was set.
    $params['facet.limit'] = variable_get('apachesolr_facet_query_limit_default', 20);
  }
}
function apachesolr_search_add_boost_params(&$params, $query, $solr) {

  // Note - we have query fields set in solrconfig.xml, which will operate when
  // none are set.
  $qf = variable_get('apachesolr_search_query_fields', array());
  $fields = $solr
    ->getFields();
  if ($qf && $fields) {
    foreach ($fields as $field_name => $field) {
      if (!empty($qf[$field_name])) {
        if ($field_name == 'body') {

          // Body is the only normed field.
          $qf[$field_name] *= 40.0;
        }
        $params['qf'][] = $field_name . '^' . $qf[$field_name];
      }
    }
  }
  $data = $solr
    ->getLuke();
  if (isset($data->index->numDocs)) {
    $total = $data->index->numDocs;
  }
  else {
    $total = db_result(db_query("SELECT COUNT(nid) FROM {node}"));
  }

  // For the boost functions for the created timestamp, etc we use the
  // standard date-biasing function, as suggested (but steeper) at
  // http://wiki.apache.org/solr/DisMaxRequestHandler
  // rord() returns 1 for the newset doc, and the number in the index for
  // the oldest doc.  The function is thus: $total/(rord()*$steepness + $total).
  $date_settings = variable_get('apachesolr_search_date_boost', '4:200.0');
  list($date_steepness, $date_boost) = explode(':', $date_settings);
  if ($date_boost) {
    $params['bf'][] = "recip(rord(created),{$date_steepness},{$total},{$total})^{$date_boost}";
  }

  // Boost on comment count.
  $comment_settings = variable_get('apachesolr_search_comment_boost', '0:0');
  list($comment_steepness, $comment_boost) = explode(':', $comment_settings);
  if ($comment_boost) {
    $params['bf'][] = "recip(rord(comment_count),{$comment_steepness},{$total},{$total})^{$comment_boost}";
  }

  // Boost for a more recent comment or node edit.
  $changed_settings = variable_get('apachesolr_search_changed_boost', '0:0');
  list($changed_steepness, $changed_boost) = explode(':', $changed_settings);
  if ($changed_boost) {
    $params['bf'][] = "recip(rord(last_comment_or_change),{$changed_steepness},{$total},{$total})^{$changed_boost}";
  }

  // Boost for nodes with sticky bit set.
  $sticky_boost = variable_get('apachesolr_search_sticky_boost', 0);
  if ($sticky_boost) {
    $params['bq'][] = "sticky:true^{$sticky_boost}";
  }

  // Boost for nodes with promoted bit set.
  $promote_boost = variable_get('apachesolr_search_promote_boost', 0);
  if ($promote_boost) {
    $params['bq'][] = "promote:true^{$promote_boost}";
  }

  // Modify the weight of results according to the node types.
  $type_boosts = variable_get('apachesolr_search_type_boosts', array());
  if (!empty($type_boosts)) {
    foreach ($type_boosts as $type => $boost) {

      // Only add a param if the boost is != 0 (i.e. > "Normal").
      if ($boost) {
        $params['bq'][] = "type:{$type}^{$boost}";
      }
    }
  }
}
function apachesolr_search_process_response($response, $query, $params) {
  $results = array();

  // We default to getting snippets from the body.
  $hl_fl = isset($params['hl.fl']) ? explode(',', $params['hl.fl']) : array(
    'body',
  );
  $total = $response->response->numFound;
  apachesolr_pager_init($total, $params['rows']);
  if ($total > 0) {
    foreach ($response->response->docs as $doc) {

      // Snippets.
      // Start with an empty snippets array.
      $snippets = array();

      // Find the nicest available snippet.
      foreach ($hl_fl as $hl_param) {
        if (isset($response->highlighting->{$doc->id}->{$hl_param})) {

          // Merge arrays preserving keys.
          foreach ($response->highlighting->{$doc->id}->{$hl_param} as $values) {
            $snippets[$hl_param] = $values;
          }
        }
      }

      // If there's no snippet at this point, add the teaser.
      if (!$snippets) {
        if (isset($doc->teaser)) {
          $snippets[] = truncate_utf8($doc->teaser, 256, TRUE);
        }
      }
      $snippet = theme('apachesolr_search_snippets', $doc, $snippets);
      if (!isset($doc->body)) {
        $doc->body = $snippet;
      }
      $doc->created = strtotime($doc->created);
      $doc->changed = strtotime($doc->changed);
      $extra = array();
      $extra['comments'] = format_plural($doc->comment_count, '1 comment', '@count comments');
      if (isset($doc->is_upload_count)) {
        $extra[] = format_plural($doc->is_upload_count, '1 attachment', '@count attachments');
      }

      // Allow modules to alter each document and its extra information.
      $data = array(
        $doc,
      );
      $data['__drupal_alter_by_ref'] = array(
        &$extra,
      );
      drupal_alter('apachesolr_search_result', $data);
      $fields = (array) $doc;
      $results[] = array(
        'link' => url($doc->path, array(
          'absolute' => TRUE,
        )),
        'type' => apachesolr_search_get_type($doc->type),
        // template_preprocess_search_result() runs check_plain() on the title
        // again.  Decode to correct the display.
        'title' => htmlspecialchars_decode($doc->title, ENT_QUOTES),
        'user' => theme('username', $doc),
        'date' => $doc->created,
        'node' => $doc,
        'extra' => $extra,
        'score' => $doc->score,
        'snippets' => $snippets,
        'snippet' => $snippet,
        'fields' => $fields,
      );
    }

    // Hook to allow modifications of the retrieved results
    foreach (module_implements('apachesolr_process_results') as $module) {
      $function = $module . '_apachesolr_process_results';
      $function($results);
    }
  }
  return $results;
}
function apachesolr_search_date_range($query, $facet_field) {
  foreach ($query
    ->get_filters($facet_field) as $filter) {

    // If we had an ISO date library we could use ISO dates
    // directly.  Instead, we convert to Unix timestamps for comparison.
    // Only use dates if we are able to parse into timestamps.
    $start = strtotime($filter['#start']);
    $end = strtotime($filter['#end']);
    if ($start && $end && $start < $end) {
      $start_iso = $filter['#start'];
      $end_iso = $filter['#end'];

      // Determine the drilldown gap for this range.
      $gap = apachesolr_date_gap_drilldown(apachesolr_date_find_query_gap($start_iso, $end_iso));
    }
  }

  // If there is no $delta field in query object, get initial
  // facet.date.* params from the DB and determine the best search
  // gap to use.  This callback assumes $delta is 'changed' or 'created'.
  if (!isset($start_iso)) {
    $start_iso = apachesolr_date_iso(db_result(db_query("SELECT MIN({$facet_field}) FROM {node} WHERE status = 1")));

    // Subtract one second, so that this range's $end_iso is not equal to the
    // next range's $start_iso.
    $end_iso = apachesolr_date_iso(db_result(db_query("SELECT MAX({$facet_field}) FROM {node} WHERE status = 1")) - 1);
    $gap = apachesolr_date_determine_gap($start_iso, $end_iso);
  }

  // Return a query range from the beginning of a gap period to the beginning
  // of the next gap period.  We ALWAYS generate query ranges of this form
  // and the apachesolr_date_*() helper functions require it.
  return array(
    "{$start_iso}/{$gap}",
    "{$end_iso}+1{$gap}/{$gap}",
    "+1{$gap}",
  );
}

/**
 * Implementation of hook_apachesolr_facets().
 *
 * Returns an array keyed by block delta.
 */
function apachesolr_search_apachesolr_facets() {
  $facets = array();
  $facets['type'] = array(
    'info' => t('Node attribute: Filter by content type'),
    'facet_field' => 'type',
  );
  $facets['uid'] = array(
    'info' => t('Node attribute: Filter by author'),
    'facet_field' => 'uid',
  );
  $facets['language'] = array(
    'info' => t('Node attribute: Filter by language'),
    'facet_field' => 'language',
  );
  $facets['changed'] = array(
    'info' => t('Node attribute: Filter by updated date'),
    'facet_field' => 'changed',
  );
  $facets['created'] = array(
    'info' => t('Node attribute: Filter by post date'),
    'facet_field' => 'created',
  );

  // A book module facet.
  if (module_exists('book')) {
    $facets['is_book_bid'] = array(
      'info' => t('Book: Filter by Book'),
      'facet_field' => 'is_book_bid',
      'content_types' => variable_get('book_allowed_types', array(
        'book',
      )),
    );
  }

  // Get taxonomy vocabulary facets.
  if (module_exists('taxonomy')) {
    $vocabs = taxonomy_get_vocabularies();
    foreach ($vocabs as $vid => $vocab) {

      // In this case the delta and facet field are the same.
      $delta = 'im_vid_' . $vid;
      $facets[$delta] = array(
        'info' => t('Taxonomy vocabulary: Filter by taxonomy @name', array(
          '@name' => $vocab->name,
        )),
        'facet_field' => $delta,
        'content_types' => $vocab->nodes,
        'display_callback' => 'apachesolr_search_taxonomy_get_term',
      );
    }
  }

  // Get CCK field facets.
  $fields = apachesolr_cck_fields();
  if ($fields) {
    foreach ($fields as $name => $field) {
      if (!empty($field['facets'])) {

        // $delta can only be 32 chars, and the CCK field name may be this
        // long also, so we cannot add anything to it.
        $placeholders = array(
          '@field_type' => $field['field_type'],
          '@field' => $field['widget']['label'],
          '@field_name' => $field['field_name'],
        );
        $facets[$field['field_name']] = array_merge($field, array(
          'info' => t('CCK @field_type field: Filter by @field (@field_name)', $placeholders),
          'facet_field' => apachesolr_index_key($field),
          'content_types' => $field['content_types'],
        ));
      }
    }
  }
  return $facets;
}

/**
 * Implementation of hook_block().
 */
function apachesolr_search_block($op = 'list', $delta = 0, $edit = array()) {
  switch ($op) {
    case 'list':
      $enabled_facets = apachesolr_get_enabled_facets('apachesolr_search');
      $facets = apachesolr_search_apachesolr_facets();

      // Add the blocks
      $blocks = array();
      foreach ($enabled_facets as $delta => $facet_field) {
        if (isset($facets[$delta])) {
          $blocks[$delta] = $facets[$delta] + array(
            'cache' => BLOCK_CACHE_PER_ROLE | BLOCK_CACHE_PER_PAGE,
          );
        }
      }
      $blocks['currentsearch'] = array(
        'info' => t('Apache Solr Search: Current search'),
        'cache' => BLOCK_CACHE_PER_PAGE,
      );
      return $blocks;
    case 'view':
      if (apachesolr_has_searched()) {

        // Get the query and response. Without these no blocks make sense.
        $response = apachesolr_static_response_cache();
        if (empty($response)) {
          return;
        }
        $query = apachesolr_current_query();
        $facets = apachesolr_get_enabled_facets('apachesolr_search');
        if (empty($facets[$delta]) && $delta != 'currentsearch') {
          return;
        }
        if (!apachesolr_block_visibility($query, 'apachesolr_search', $delta)) {
          return;
        }

        // Handle taxonomy vocabulary facets
        if (strpos($delta, 'im_vid_') === 0) {
          return apachesolr_search_taxonomy_facet_block($response, $query, $delta);
        }
        switch ($delta) {
          case 'currentsearch':
            return apachesolr_search_currentsearch_block($response, $query);
          case 'is_book_bid':
            return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by book'), 'apachesolr_search_get_book');
          case 'language':
            return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by language'), 'apachesolr_search_language_name');
          case 'uid':
            return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by author'), 'apachesolr_search_get_username');
          case 'type':
            return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by type'), 'apachesolr_search_get_type');
          case 'changed':
            return apachesolr_date_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by modification date'));
          case 'created':
            return apachesolr_date_facet_block($response, $query, 'apachesolr_search', $delta, $delta, t('Filter by post date'));
          default:
            if ($fields = apachesolr_cck_fields()) {
              foreach ($fields as $name => $field) {
                if ($field['field_name'] == $delta) {
                  $index_key = apachesolr_index_key($field);
                  $callback = isset($field['display_callback']) ? $field['display_callback'] : FALSE;
                  $block_function = isset($field['facet_block_callback']) && function_exists($field['facet_block_callback']) ? $field['facet_block_callback'] : 'apachesolr_facet_block';
                  return $block_function($response, $query, 'apachesolr_search', $delta, $index_key, t('Filter by @field', array(
                    '@field' => $field['widget']['label'],
                  )), $callback);
                }
              }
            }
        }
        break;
      }
      break;
    case 'configure':
      if ($delta != 'currentsearch') {
        return apachesolr_facetcount_form('apachesolr_search', $delta);
      }
      break;
    case 'save':
      if ($delta != 'currentsearch') {
        apachesolr_facetcount_save($edit);
      }
      break;
  }
}

/**
 * Generate a list including the field and all its children.
 */
function apachesolr_search_collect_children($field) {
  $remove[] = $field;
  if (!empty($field['#children'])) {
    foreach ($field['#children'] as $child_field) {
      $remove = array_merge($remove, apachesolr_search_collect_children($child_field));
    }
  }
  return $remove;
}

/**
 * Generate the facet block for a taxonomy vid delta.
 */
function apachesolr_search_taxonomy_facet_block($response, $query, $delta) {
  $vid = substr($delta, 7);
  if (!module_exists('taxonomy') || !is_numeric($vid)) {
    return;
  }

  // Check that we have a response and a valid vid.
  if (is_object($response->facet_counts->facet_fields->{$delta}) && ($vocab = taxonomy_vocabulary_load($vid))) {
    $reflect_hierarchy = apachesolr_search_get_hierarchical_vocabularies();
    $contains_active = FALSE;
    $facets = array();
    foreach ($response->facet_counts->facet_fields->{$delta} as $tid => $count) {

      // TODO - for now we don't handle facet missing.
      if ($tid != '_empty_') {
        $active = $query
          ->has_filter('tid', $tid);
        if ($active) {
          $contains_active = TRUE;
        }
        $facets[$tid] = array(
          '#name' => 'tid',
          '#value' => $tid,
          '#exclude' => FALSE,
          '#count' => $count,
          '#parent' => 0,
          '#children' => array(),
          '#has_children' => FALSE,
          '#active' => $active,
        );
      }
    }
    if ($facets && $reflect_hierarchy[$vocab->vid]) {
      $placeholders = db_placeholders($facets);
      $tids = array_keys($facets);

      // @todo: faster as 2x separate queries?
      $result = db_query("SELECT tid, parent FROM {term_hierarchy} WHERE parent > 0 AND (tid IN ({$placeholders}) OR parent IN ({$placeholders}))", array_merge($tids, $tids));
      while ($term = db_fetch_object($result)) {

        // Mark all terms that are parents for later CSS class.
        // We assume data in the Solr index is complete - potential for some
        // breakage here.
        if (isset($facets[$term->parent])) {
          $facets[$term->parent]['#has_children'] = TRUE;
          if (isset($facets[$term->tid])) {
            $facets[$term->tid]['#parent'] = $term->parent;

            // Use a reference so we see the updated data.
            $facets[$term->parent]['#children'][] =& $facets[$term->tid];
          }
        }
      }

      // Check for the case like starting on a taxonomy/term/$tid page
      // where parents are not marked as active.
      // @todo: can we make this more efficient?
      do {
        $added_active = FALSE;
        foreach ($facets as $tid => $field) {
          if ($field['#active'] && $field['#parent'] && !$facets[$field['#parent']]['#active']) {

            // This parent has an active child.
            $added_active = TRUE;
            $query
              ->add_filter('tid', $field['#parent']);
            $facets[$field['#parent']]['#active'] = TRUE;
          }
        }
      } while ($added_active);
      foreach ($facets as $tid => $field) {
        if (!empty($field['#parent'])) {

          // We will render it via its parent.
          unset($facets[$tid]);
        }
      }
    }
    $items = apachesolr_search_nested_facet_items($query, $facets, $response->response->numFound);

    // Process all terms into an item list
    if ($items && ($response->response->numFound > 1 || $contains_active)) {
      $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
      $limit = isset($initial_limits['apachesolr_search'][$delta]) ? $initial_limits['apachesolr_search'][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
      return array(
        'subject' => t('Filter by @field', array(
          '@field' => $vocab->name,
        )),
        'content' => theme('apachesolr_facet_list', $items, $limit, $delta),
      );
    }
  }
}

/**
 * Recursive function that returns a nested array of facet values for use with
 * theme_item_list().
 *
 * @param $query
 *   The current Solr query.
 * @param $facets
 *   Array of facet items to prepare for rendering, possibly as nested lists.
 * @param $num_found
 *   The number of documents in the current response.
 * @param $sort
 *   If true, the returned list will be sorted based on the count of each
 *   facets, it's text representation and wither it's active.  If false,
 *   the facets will be returned in the order they were received.
 */
function apachesolr_search_nested_facet_items($query, $facets, $num_found, $sort = TRUE) {
  $facet_query_sorts = variable_get('apachesolr_facet_query_sorts', array());
  $facet_show_children = variable_get('apachesolr_facet_show_children', array());
  $items = array();
  foreach ($facets as $field) {
    $facet_text = '';
    $field_name = $field['#name'];
    if (function_exists('taxonomy_get_term')) {

      // Taxonomy filters in the query string use the tid field.
      if ($field_name == 'tid') {

        // Each taxonomy vocabulary in the Solr index uses its own field.
        $term = taxonomy_get_term($field['#value']);
        $field_name = 'im_vid_' . $term->vid;
      }
    }
    $facet_definition = apachesolr_get_facet_definition_by_field_name($field_name);
    if (isset($facet_definition['display_callback'])) {
      $function = $facet_definition['display_callback'];
      if (function_exists($function)) {
        $facet_text = $function($field['#value'], $field);
      }
    }
    if (!$facet_text) {
      $breadcrumb_name = 'apachesolr_breadcrumb_' . $field['#name'];
      drupal_alter('apachesolr_theme_breadcrumb', $breadcrumb_name);
      $facet_text = theme($breadcrumb_name, $field, $field['#exclude']);
    }
    if (!$facet_text) {
      $facet_text = $field['#value'];
    }
    $active = !empty($field['#active']);
    $link = array();
    $new_query = clone $query;
    if ($active) {
      foreach (apachesolr_search_collect_children($field) as $child) {
        $new_query
          ->remove_filter($child['#name'], $child['#value']);
      }
      $options['query'] = $new_query
        ->get_url_queryvalues();
      $link['data'] = theme('apachesolr_unclick_link', $facet_text, $new_query
        ->get_path(), $options);
    }
    else {
      $new_query
        ->add_filter($field['#name'], $field['#value']);
      $options = array(
        'query' => $new_query
          ->get_url_queryvalues(),
      );
      $link['data'] = theme('apachesolr_facet_link', $facet_text, $new_query
        ->get_path(), $options, $field['#count'], FALSE, $num_found);
    }
    if ($active) {

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

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

      // If this block is to be alphabetically/numerically sorted by value, change $sortpre.
      $sortpre = $facet_text;
    }
    else {
      $sortpre = 1000000 - $field['#count'];
    }

    // Only display children if the block enables it or the parent is clicked.
    if (isset($facet_show_children['apachesolr_search'][$field_name]) && $facet_show_children['apachesolr_search'][$field_name] || !empty($field['#children']) && $field['#active'] == TRUE) {
      $link['children'] = apachesolr_search_nested_facet_items($query, $field['#children'], $num_found, $sort);
      $link['class'] = "expanded-facet";
    }
    elseif (!empty($field['#has_children'])) {
      $link['class'] = "collapsed";
    }
    $items[$sortpre . '*' . $facet_text . $field['#name'] . $field['#value']] = $link;
  }
  if ($sort && $items) {
    switch ($facet_query_sorts['apachesolr_search'][$field_name]) {
      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;
    }
  }
  return array_values($items);
}
function apachesolr_search_taxonomy_get_term($tid) {
  if (function_exists('taxonomy_get_term')) {
    $term = taxonomy_get_term($tid);
    return $term->name;
  }
}

/**
 * Callback function for the 'Filter by book' facet block.
 */
function apachesolr_search_get_book($facet, &$options) {
  if (is_numeric($facet)) {
    return db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $facet));
  }
  else {
    $options['html'] = TRUE;
    return theme('placeholder', t('Not in any book'));
  }
}
function apachesolr_search_language_name($lang) {
  static $list = NULL;
  if (!isset($list)) {
    if (function_exists('locale_language_list')) {
      $list = locale_language_list();
    }
    $list['und'] = t('Language neutral');
  }
  return $lang && isset($list[$lang]) ? $list[$lang] : $lang;
}

/**
 * Callback function for the 'Filter by name' facet block.
 */
function apachesolr_search_get_username($facet) {
  if (is_numeric($facet)) {
    return theme('apachesolr_breadcrumb_uid', array(
      '#value' => $facet,
    ));
  }
  return '';
}

/**
 * Callback function for the 'Filter by type' facet block.
 */
function apachesolr_search_get_type($facet) {
  $type = node_get_types('name', $facet);
  if ($type === FALSE && $facet == 'comment') {
    $type = t('Comment');
  }

  // A disabled or missing node type returns FALSE.
  $name = $type === FALSE ? $facet : $type;
  return apachesolr_tt("nodetype:type:{$facet}:name", $name);
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * This adds the 0 option to the search admin form.
 */
function apachesolr_search_form_search_admin_settings_alter(&$form, $form_state) {
  $form['indexing_throttle']['search_cron_limit']['#options']['0'] = '0';
  ksort($form['indexing_throttle']['search_cron_limit']['#options']);
}

/**
 * Implementation of hook_form_[form_id]_alter().
 */
function apachesolr_search_form_search_theme_form_alter(&$form, $form_state) {
  apachesolr_search_form_search_block_form_alter($form, $form_state);
}

/**
 * Implementation of hook_form_[form_id]_alter().
 */
function apachesolr_search_form_search_block_form_alter(&$form, $form_state) {
  if (variable_get('apachesolr_search_make_default', 0)) {
    $form['#action'] = url('search/apachesolr_search');
    if (!isset($form['#submit'])) {
      $form['#submit'] = array(
        'apachesolr_search_search_box_form_submit',
      );
    }
    else {
      $key = array_search('search_box_form_submit', $form['#submit']);
      if ($key !== FALSE) {

        // Replace the search module's function.
        $form['#submit'][$key] = 'apachesolr_search_search_box_form_submit';
      }
    }
  }
}

/**
 * Process a block search form submission.
 *
 * @see search_box_form_submit()
 */
function apachesolr_search_search_box_form_submit($form, &$form_state) {

  // The search form relies on control of the redirect destination for its
  // functionality, so we override any static destination set in the request,
  // for example by drupal_access_denied() or drupal_not_found()
  // (see http://drupal.org/node/292565).
  if (isset($_REQUEST['destination'])) {
    unset($_REQUEST['destination']);
  }
  if (isset($_REQUEST['edit']['destination'])) {
    unset($_REQUEST['edit']['destination']);
  }
  $form_id = $form['form_id']['#value'];
  $keys = $form_state['values'][$form_id];

  // Handle Apache webserver clean URL quirks.
  if (variable_get('clean_url', '0')) {
    $keys = str_replace('+', '%2B', $keys);
  }
  $form_state['redirect'] = 'search/apachesolr_search/' . trim($keys);
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * This adds spelling suggestions, retain filters to the search form.
 */
function apachesolr_search_form_search_form_alter(&$form, $form_state) {
  if ($form['module']['#value'] == 'apachesolr_search') {
    $form['#submit'] = array(
      'apachesolr_search_form_search_submit',
    );

    // No other modification make sense unless a query is active.
    // Note - this means that the query must always be run before
    // calling drupal_get_form('search_form').
    $apachesolr_has_searched = apachesolr_has_searched();
    $queryvalues = array();
    if ($apachesolr_has_searched) {
      $query = apachesolr_current_query();
      $queryvalues = $query
        ->get_url_queryvalues();
    }
    $form['basic']['apachesolr_search']['#tree'] = TRUE;
    $form['basic']['apachesolr_search']['queryvalues'] = array(
      '#type' => 'hidden',
      // We use JSON encoding instead of PHP serialize, since otherwise we be
      // at risk of user input being injected into the hidden string and
      // unserialized.
      '#default_value' => json_encode($queryvalues),
    );
    $form['basic']['apachesolr_search']['get'] = array(
      '#type' => 'hidden',
      '#default_value' => json_encode(array_diff_key($_GET, array(
        'q' => 1,
        'page' => 1,
        'filters' => 1,
        'solrsort' => 1,
        'retain-filters' => 1,
      ))),
    );
    if ($queryvalues || isset($form_state['post']['apachesolr_search']['retain-filters'])) {
      $form['basic']['apachesolr_search']['retain-filters'] = array(
        '#type' => 'checkbox',
        '#title' => t('Retain current filters'),
        '#default_value' => (int) isset($_GET['retain-filters']),
      );
    }
    if (variable_get('apachesolr_search_spellcheck', TRUE) && $apachesolr_has_searched && ($response = apachesolr_static_response_cache())) {

      // Get spellchecker suggestions into an array.
      if (isset($response->spellcheck->suggestions) && $response->spellcheck->suggestions) {
        $suggestions = get_object_vars($response->spellcheck->suggestions);
        if ($suggestions) {

          // Get the original query and replace words.
          $query = apachesolr_current_query();
          foreach ($suggestions as $word => $value) {
            $replacements[$word] = $value->suggestion[0];
          }
          $new_keywords = strtr($query
            ->get_query_basic(), $replacements);

          // Show only if suggestion is different than current query.
          if ($query
            ->get_query_basic() != $new_keywords) {
            $form['basic']['suggestion'] = array(
              '#prefix' => '<div class="spelling-suggestions">',
              '#suffix' => '</div>',
              '#type' => 'item',
              '#title' => t('Did you mean'),
              '#value' => l($new_keywords, $query
                ->get_path($new_keywords)),
            );
          }
        }
      }
    }
  }
}

/**
 * Added form submit function to account for Apache mode_rewrite quirks.
 *
 * @see apachesolr_search_form_search_form_alter()
 */
function apachesolr_search_form_search_submit($form, &$form_state) {
  $fv = $form_state['values'];
  $keys = $fv['processed_keys'];
  $base = 'search/' . $fv['module'] . '/';
  if (variable_get('clean_url', '0')) {
    $keys = str_replace('+', '%2B', $keys);
  }
  $get = json_decode($fv['apachesolr_search']['get'], TRUE);
  $queryvalues = json_decode($fv['apachesolr_search']['queryvalues'], TRUE);
  if (!empty($fv['apachesolr_search']['retain-filters']) && $queryvalues) {
    $get = $queryvalues + $get;
    $get['retain-filters'] = '1';
  }
  $form_state['redirect'] = array(
    $base . $keys,
    $get,
  );

  // TODO decide if we want to make the facet blocks in the well an option.
  //  if ($keys == '' && !$queryvalues) {
  //    form_set_error('keys', t('Please enter some keywords.'));
  //  }
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * This adds options to the apachesolr admin form.
 */
function apachesolr_search_form_apachesolr_settings_alter(&$form, $form_state) {
  $form['apachesolr_search_browse'] = array(
    '#type' => 'radios',
    '#title' => t('Behavior on empty search'),
    '#options' => array(
      'none' => t("Show search box"),
      'browse' => t("Show search box and enabled filters' blocks under the search box"),
      'blocks' => t("Show search box and enabled filters' blocks in their configured regions"),
      'results' => t("Show search box, enabled filters' blocks in their configured regions and first page of all available results"),
    ),
    '#default_value' => variable_get('apachesolr_search_browse', 'browse'),
    '#description' => t("This is what is shown when the user enters an empty search, or removes all filters from an active search. Remember to enable filters on the !filterslink and assign blocks to regions on the !blocklink", array(
      '!filterslink' => l('enabled filters page', 'admin/settings/apachesolr/enabled-filters'),
      '!blocklink' => l('block settings page', 'admin/build/block'),
    )),
  );
  $form['advanced']['apachesolr_search_make_default'] = array(
    '#type' => 'radios',
    '#title' => t('Make Apache Solr Search the default'),
    '#default_value' => variable_get('apachesolr_search_make_default', 0),
    '#options' => array(
      0 => t('Disabled'),
      1 => t('Enabled'),
    ),
    '#description' => t('Hides core node search, and makes the search block submit to Apache Solr Search'),
  );
  $form['advanced']['apachesolr_search_default_previous'] = array(
    '#type' => 'value',
    '#value' => variable_get('apachesolr_search_make_default', 0),
  );
  $form['advanced']['apachesolr_search_taxonomy_links'] = array(
    '#type' => 'radios',
    '#title' => t('Use Apache Solr for taxonomy links'),
    '#default_value' => variable_get('apachesolr_search_taxonomy_links', 0),
    '#description' => t('Note: Vocabularies that need this behavior need to be checked off on the <a href="@enabled_filters_url">enabled filters</a> settings page. Note that content types ommitted from the Apache Solr index will not be shown.', array(
      '@enabled_filters_url' => url('admin/settings/apachesolr/enabled-filters'),
    )),
    '#options' => array(
      0 => t('Disabled'),
      1 => t('Enabled'),
    ),
  );
  $form['advanced']['apachesolr_search_taxonomy_previous'] = array(
    '#type' => 'value',
    '#value' => variable_get('apachesolr_search_taxonomy_links', 0),
  );
  $form['apachesolr_search_spellcheck'] = array(
    '#type' => 'checkbox',
    '#title' => t('Enable spellchecker and suggestions'),
    '#default_value' => variable_get('apachesolr_search_spellcheck', TRUE),
    '#description' => t('Enable spellchecker and get word suggestions. Also known as the "Did you mean ... ?" feature.'),
  );
  $form['#submit'][] = 'apachesolr_search_build_spellcheck';
  $form['#submit'][] = 'apachesolr_search_make_default_submit';

  // Move buttons to the bottom.
  $buttons = $form['buttons'];
  unset($form['buttons']);
  $form['buttons'] = $buttons;
}

/**
 * Form submit funtion - do a menu rebuild if needed.
 *
 * @see apachesolr_search_form_apachesolr_settings_alter()
 */
function apachesolr_search_make_default_submit($form, &$form_state) {

  // We use variable_get() instead of the form values so as to also handle reset to defaults.
  if ($form_state['values']['apachesolr_search_default_previous'] != variable_get('apachesolr_search_make_default', 0) || $form_state['values']['apachesolr_search_taxonomy_previous'] != variable_get('apachesolr_search_taxonomy_links', 0)) {

    // Take account of path changes
    menu_rebuild();
  }
}

/**
 * Implementation of hook_form_[form_id]_alter().
 *
 * Rebuild (empty) the spellcheck dictionary when the index is deleted..
 */
function apachesolr_search_form_apachesolr_delete_index_form_alter(&$form, $form_state) {
  $form['submit']['#submit'][] = 'apachesolr_search_build_spellcheck';
}
function apachesolr_search_build_spellcheck() {
  try {
    $solr = apachesolr_get_solr();
    $params['spellcheck'] = 'true';
    $params['spellcheck.build'] = 'true';
    $response = $solr
      ->search('solr', 0, 0, $params);
  } catch (Exception $e) {
    watchdog('Apache Solr', nl2br(check_plain($e
      ->getMessage())), NULL, WATCHDOG_ERROR);
  }
}

/**
 * Implementation of hook_theme().
 */
function apachesolr_search_theme() {
  return array(
    'apachesolr_breadcrumb_is_book_bid' => array(
      'arguments' => array(
        'field' => NULL,
        'exclude' => FALSE,
      ),
    ),
    'apachesolr_breadcrumb_uid' => array(
      'arguments' => array(
        'field' => NULL,
        'exclude' => FALSE,
      ),
    ),
    'apachesolr_breadcrumb_tid' => array(
      'arguments' => array(
        'field' => NULL,
        'exclude' => FALSE,
      ),
    ),
    'apachesolr_breadcrumb_type' => array(
      'arguments' => array(
        'field' => NULL,
        'exclude' => FALSE,
      ),
    ),
    'apachesolr_breadcrumb_language' => array(
      'arguments' => array(
        'lang' => NULL,
        'exclude' => FALSE,
      ),
    ),
    'apachesolr_breadcrumb_cck' => array(
      'arguments' => array(
        'field' => NULL,
      ),
    ),
    'apachesolr_breadcrumb_changed' => array(
      'arguments' => array(
        'field' => NULL,
      ),
      'function' => 'theme_apachesolr_breadcrumb_date_range',
    ),
    'apachesolr_breadcrumb_created' => array(
      'arguments' => array(
        'field' => NULL,
      ),
      'function' => 'theme_apachesolr_breadcrumb_date_range',
    ),
    'apachesolr_browse_blocks' => array(
      'arguments' => array(
        'blocks' => NULL,
      ),
    ),
    'apachesolr_currentsearch' => array(
      'arguments' => array(
        'total_found' => NULL,
        'links' => NULL,
      ),
    ),
    'apachesolr_search_snippets' => array(
      'arguments' => array(
        'doc' => NULL,
        'snippets' => array(),
      ),
    ),
    'apachesolr_search_noresults' => array(
      'arguments' => array(),
    ),
    'apachesolr_search_results_page' => array(
      'arguments' => array(
        'form' => '',
        'results' => '',
      ),
    ),
  );
}

/**
 * Alters the function used to theme breadcrumbs
 * @param string $fieldname
 *
 */
function apachesolr_search_apachesolr_theme_breadcrumb_alter(&$fieldname) {
  $matches = preg_split('/_cck_/', $fieldname);
  if (isset($matches[1])) {
    $fieldname = 'apachesolr_breadcrumb_cck';
  }
}

/**
 * Theme function for CCK fields in breadcrumbs.
 * TODO: The logic for getting here is too convoluted, and
 * there are too many bizarre naming conventions in play (_cck_, _end).
 * The checks for _cck_ and _end MUST get refactored.
 */
function theme_apachesolr_breadcrumb_cck($field) {
  $matches = preg_split('/_cck_/', $field['#name']);
  if (isset($matches[1])) {
    $match = $matches[1];

    // TODO: If the apachesolr_date module is present we might
    // have fields with the suffix '_end'. These are the end-dates
    // and need the suffix removed.
    if (module_exists('apachesolr_date')) {
      $match = preg_replace('/_end{1}$/', '', $match);
    }
    $mappings = apachesolr_cck_fields();
    if (isset($mappings[$match]['display_callback'])) {
      $function = $mappings[$match]['display_callback'];
      if (function_exists($function)) {
        $facet = $field['#value'];
        $options = array_merge($mappings[$match], array(
          'delta' => $matches[1],
        ));
        return $function($facet, $options);
      }
    }
  }
  return $field['#value'];
}
function theme_apachesolr_breadcrumb_language($field, $exclude = FALSE) {
  return apachesolr_search_language_name($field['#value']);
}

/**
 * Proxy theme function for 'created' and 'changed' date fields.
 */
function theme_apachesolr_breadcrumb_date_range($field) {
  if (preg_match('@[\\[\\{](\\S+) TO (\\S+)[\\]\\}]@', $field['#value'], $match)) {
    return apachesolr_date_format_range($match[1], $match[2]);
  }
  return $field['#value'];
}

/**
 * Return the username from $uid
 */
function theme_apachesolr_breadcrumb_uid($field) {
  if ($field['#value'] == 0) {
    return variable_get('anonymous', t('Anonymous'));
  }
  else {
    return db_result(db_query("SELECT name FROM {users} WHERE uid = %d", $field['#value']));
  }
}

/**
 * Return the term name from $tid, or $tid as a fallback.
 */
function theme_apachesolr_breadcrumb_tid($field) {
  if (function_exists('taxonomy_get_term')) {
    if ($term = taxonomy_get_term($field['#value'])) {
      return $term->name;
    }
  }
  return $field['#value'];
}

/**
 * Return the human readable text for a content type.
 */
function theme_apachesolr_breadcrumb_type($field) {
  $name = node_get_types('name', $field['#value']);
  return apachesolr_tt("nodetype:type:{$field['#value']}:name", $name);
}

/**
 * Return the title of a book.
 */
function theme_apachesolr_breadcrumb_is_book_bid($field) {
  if (is_numeric($field['#value'])) {
    return db_result(db_query('SELECT title FROM {node} WHERE nid = %d', $field['#value']));
  }
  else {
    return t('Not in any book');
  }
}

/**
 * Return current search block contents
 */
function theme_apachesolr_currentsearch($total_found, $links) {
  return theme_item_list($links, format_plural($total_found, 'Search found 1 item', 'Search found @count items'));
}

/**
 * Theme the highlighted snippet text for a search entry.
 *
 * @param object $doc
 * @param array $snippets
 *
 */
function theme_apachesolr_search_snippets($doc, $snippets = array()) {
  $result = '';
  if (isset($snippets['body'])) {
    $result .= $snippets['body'];
    unset($snippets['body']);
  }
  if (isset($snippets['teaser'])) {
    $result .= strlen($result) > 0 ? ' ... ' : '';
    $result .= $snippets['teaser'];
    unset($snippets['teaser']);
  }
  $result .= strlen($result) > 0 ? ' ... ' : '';
  return $result . implode(' ... ', $snippets) . ' ...';
}

/**
 * Theme function for the search results page.
 */
function theme_apachesolr_search_results_page($form, $results) {
  return $form . $results;
}

/**
 * Brief message to display when no results match the query.
 *
 * @see search_help()
 */
function theme_apachesolr_search_noresults() {
  return t('<ul>
<li>Check if your spelling is correct, or try removing filters.</li>
<li>Remove quotes around phrases to match each word individually: <em>"blue drop"</em> will match less than <em>blue drop</em>.</li>
<li>You can require or exclude terms using + and -: <em>big +blue drop</em> will require a match on <em>blue</em> while <em>big blue -drop</em> will exclude results that contain <em>drop</em>.</li>
</ul>');
}
function apachesolr_get_parent_terms($tids) {

  // Find the starting tid terms and then all their parents.
  $parent_terms = array();
  $new_tids = $tids;
  do {
    $result = db_query(db_rewrite_sql("SELECT t.tid, t.parent FROM {term_hierarchy} t WHERE t.tid IN (" . db_placeholders($new_tids) . ")", 't', 'tid'), $new_tids);
    $new_tids = array();
    while ($term = db_fetch_object($result)) {
      $parent_terms[$term->tid] = $term;
      if ($term->parent > 0) {
        $new_tids[] = $term->parent;
      }
    }
  } while ($new_tids);
  return $parent_terms;
}

/**
 * Return the contents of the "Current search" block.
 *
 * @param $response
 *   The Solr response object.
 * @param $query
 *   The Solr query object.
 */
function apachesolr_search_currentsearch_block($response, $query) {
  $fields = $query
    ->get_filters();
  $links = array();
  $facets = array();

  // If current search has keys, offer current search without them
  if ($keys = $query
    ->get_query_basic()) {
    $links[] = theme('apachesolr_unclick_link', $keys, $query
      ->get_path(''), array(
      'query' => $query
        ->get_url_queryvalues(),
    ));
  }

  // Find all taxonomy terms to be treated in a hierarchy.
  if (module_exists('taxonomy')) {
    $reflect_hierarchy = apachesolr_search_get_hierarchical_vocabularies();
    $facets = array();
    foreach ($fields as $index => $field) {
      if ($field['#name'] && 'tid' == $field['#name']) {
        $term = taxonomy_get_term($field['#value']);
        if ($reflect_hierarchy[$term->vid]) {
          $fields[$index] += array(
            '#parent' => 0,
            '#children' => array(),
          );

          // Just save the index for later lookup.
          $facets[$term->tid] = $index;
        }
      }
    }
    if ($facets) {

      // Get all term hierarchy information.
      $all_terms = apachesolr_get_parent_terms(array_keys($facets));
      foreach ($all_terms as $tid => $term) {
        if (!isset($facets[$tid])) {

          // This is a parent that is missing from the query.  E.g. we started
          // on a taxonomy/term/$tid page.
          $query
            ->add_filter('tid', $tid);

          // Ordering is wonky, but oh well...
          $fields[] = array(
            '#name' => 'tid',
            '#value' => $tid,
            '#exclude' => FALSE,
            '#parent' => 0,
            '#children' => array(),
          );

          // Get the index of the newly added facet.
          end($fields);
          $facets[$tid] = key($fields);
        }
      }
      foreach ($all_terms as $tid => $term) {
        $index = $facets[$term->tid];
        if (isset($facets[$term->parent])) {

          // Use a reference so we see the updated data.
          $fields[$facets[$term->parent]]['#children'][] =& $fields[$index];
          $fields[$index]['#parent'] = $term->parent;
        }
      }
    }
  }

  // We don't directly render any items with a parent.
  foreach ($fields as $index => $field) {
    $fields[$index]['#active'] = TRUE;
    if (!empty($fields[$index]['#parent']) || !$field['#name']) {

      // We will render it via its parent.
      unset($fields[$index]);
    }
  }
  $links = array_merge($links, apachesolr_search_nested_facet_items($query, $fields, $response->response->numFound, FALSE));
  if ($links) {
    $content = theme('apachesolr_currentsearch', $response->response->numFound, $links);
    return array(
      'subject' => t('Current search'),
      'content' => $content,
    );
  }
}

/**
 * Return an array of taxonomy facets that should be displayed hierarchically.
 */
function apachesolr_search_get_hierarchical_vocabularies() {
  static $result;
  if (!isset($result)) {
    $result = array();
    if (function_exists('taxonomy_get_vocabularies')) {
      $vocabularies = taxonomy_get_vocabularies();
      $force_flat = variable_get('apachesolr_search_force_flat_vocabularies', array());
      foreach ($vocabularies as $voc) {

        // If the vocabulary is not multiple-parent hierarchical and not
        // freetagging and not designated to be forced to display flat.
        if ($voc->hierarchy != 2 && $voc->tags != 1 && empty($force_flat[$voc->vid])) {
          $result[$voc->vid] = 1;
        }
      }
    }
  }
  return $result;
}

Functions

Namesort descending Description
apachesolr_get_parent_terms
apachesolr_search_add_boost_params
apachesolr_search_add_facet_params
apachesolr_search_apachesolr_document_handlers Implementation of hook_apachesolr_document_handlers().
apachesolr_search_apachesolr_facets Implementation of hook_apachesolr_facets().
apachesolr_search_apachesolr_theme_breadcrumb_alter Alters the function used to theme breadcrumbs
apachesolr_search_apachesolr_types_exclude Implementation of hook_apachesolr_types_exclude().
apachesolr_search_basic_params
apachesolr_search_block Implementation of hook_block().
apachesolr_search_browse Execute an empty search (match all documents) and show a listing of all enabled facets.
apachesolr_search_build_spellcheck
apachesolr_search_collect_children Generate a list including the field and all its children.
apachesolr_search_cron Implementation of hook_cron(). Indexes nodes.
apachesolr_search_currentsearch_block Return the contents of the "Current search" block.
apachesolr_search_date_range
apachesolr_search_execute Execute a search results based on keyword, filter, and sort strings.
apachesolr_search_finalize_query Implementation of hook_finalize_query().
apachesolr_search_form_apachesolr_delete_index_form_alter Implementation of hook_form_[form_id]_alter().
apachesolr_search_form_apachesolr_settings_alter Implementation of hook_form_[form_id]_alter().
apachesolr_search_form_search_admin_settings_alter Implementation of hook_form_[form_id]_alter().
apachesolr_search_form_search_block_form_alter Implementation of hook_form_[form_id]_alter().
apachesolr_search_form_search_form_alter Implementation of hook_form_[form_id]_alter().
apachesolr_search_form_search_submit Added form submit function to account for Apache mode_rewrite quirks.
apachesolr_search_form_search_theme_form_alter Implementation of hook_form_[form_id]_alter().
apachesolr_search_get_book Callback function for the 'Filter by book' facet block.
apachesolr_search_get_hierarchical_vocabularies Return an array of taxonomy facets that should be displayed hierarchically.
apachesolr_search_get_type Callback function for the 'Filter by type' facet block.
apachesolr_search_get_username Callback function for the 'Filter by name' facet block.
apachesolr_search_help Implementation of hook_help().
apachesolr_search_highlighting_params Add highlighting settings to the search params.
apachesolr_search_language_name
apachesolr_search_make_default_submit Form submit funtion - do a menu rebuild if needed.
apachesolr_search_menu Implementation of hook_menu().
apachesolr_search_menu_alter Implementation of hook_menu_alter().
apachesolr_search_nested_facet_items Recursive function that returns a nested array of facet values for use with theme_item_list().
apachesolr_search_process_response
apachesolr_search_search Implementation of hook_search().
apachesolr_search_search_box_form_submit Process a block search form submission.
apachesolr_search_spellcheck_params
apachesolr_search_taxonomy_facet_block Generate the facet block for a taxonomy vid delta.
apachesolr_search_taxonomy_get_term
apachesolr_search_theme Implementation of hook_theme().
apachesolr_search_view Re-implementation of search_view().
theme_apachesolr_breadcrumb_cck Theme function for CCK fields in breadcrumbs. TODO: The logic for getting here is too convoluted, and there are too many bizarre naming conventions in play (_cck_, _end). The checks for _cck_ and _end MUST get refactored.
theme_apachesolr_breadcrumb_date_range Proxy theme function for 'created' and 'changed' date fields.
theme_apachesolr_breadcrumb_is_book_bid Return the title of a book.
theme_apachesolr_breadcrumb_language
theme_apachesolr_breadcrumb_tid Return the term name from $tid, or $tid as a fallback.
theme_apachesolr_breadcrumb_type Return the human readable text for a content type.
theme_apachesolr_breadcrumb_uid Return the username from $uid
theme_apachesolr_browse_blocks Theming function that shows a groups of blocks so users can start a search from a filter.
theme_apachesolr_currentsearch Return current search block contents
theme_apachesolr_search_noresults Brief message to display when no results match the query.
theme_apachesolr_search_results_page Theme function for the search results page.
theme_apachesolr_search_snippets Theme the highlighted snippet text for a search entry.