You are here

apachesolr_search.module in Apache Solr Search 5.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.
 */

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

/**
 * 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)) {
        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/logs/status/run-cron', 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($may_cache) {
  $items = array();
  if ($may_cache) {
    $items[] = array(
      'path' => 'admin/settings/apachesolr/query-fields',
      'title' => t('Search fields'),
      'callback' => 'apachesolr_search_settings_page',
      'access' => user_access('administer search'),
      'weight' => 1,
      'type' => MENU_LOCAL_TASK,
    );
    $items[] = array(
      'path' => 'admin/settings/apachesolr/content-bias',
      'title' => t('Content bias settings'),
      'callback' => 'apachesolr_boost_settings_page',
      'access' => user_access('administer search'),
      'weight' => 1,
      'type' => MENU_LOCAL_TASK,
    );
  }
  return $items;
}

/**
 * Implementation of hook_update_index().
 */
function apachesolr_search_update_index() {
  $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())), WATCHDOG_ERROR);
        apachesolr_failure(t('Solr search'), $keys);
      }
      break;
  }

  // switch
}

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

  // Search form submits with POST but redirects to GET.
  $results = '';
  if (!isset($_POST['form_id'])) {
    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());
    $filters = '';
    if (isset($_GET['filters'])) {
      $filters = trim($_GET['filters']);
    }

    // Only perform search if there is non-whitespace search term or filters:
    if ($keys || $filters) {

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

      // Collect the search results:
      $results = search_data($keys, $type);
      if ($results) {
        $results = theme('box', t('Search results'), $results);
      }
      else {
        $results = theme('box', t('Your search yielded no results'), variable_get('apachesolr_search_noresults', apachesolr_search_noresults()));
      }
    }
  }

  // Construct the search form.
  return drupal_get_form('search_form', NULL, $keys, $type) . $results;
}
function 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>');
}

/**
 * Execute a search results based on keyword, filter, and sort strings.
 *
 * @throws Exception
 */
function apachesolr_search_execute($keys, $filters, $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, $filters, $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';
  }
  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(menu_get_active_breadcrumb(), $query
    ->get_breadcrumb()));
  return apachesolr_process_response($response, $final_query, $params);
}
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_fieldtohightlight', NULL);
  return $params;
}
function apachesolr_search_spellcheck_params($query) {
  $params = array();
  if (variable_get('apachesolr_search_spellcheck', FALSE)) {

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

      // TODO: generalize handling of date and range facets.
      if ($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 {
        $params['facet.field'][] = $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_process_response($response, $query, $params) {
  $results = array();

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

      // Find the nicest available snippet.
      if (isset($response->highlighting->{$doc->id}->{$hl_fl})) {
        $snippet = theme('apachesolr_search_snippets', $doc, $response->highlighting->{$doc->id}->{$hl_fl});
      }
      elseif (isset($doc->teaser)) {
        $snippet = theme('apachesolr_search_snippets', $doc, array(
          truncate_utf8($doc->teaser, 256, TRUE),
        ));
      }
      else {
        $snippet = '';
      }
      if (!isset($doc->body)) {
        $doc->body = $snippet;
      }
      $doc->created = strtotime($doc->created);
      $doc->changed = strtotime($doc->changed);

      // Allow modules to alter each document.
      drupal_alter('apachesolr_search_result', $doc);
      $fields = array();
      foreach ($doc
        ->getFieldNames() as $field_name) {
        $fields[$field_name] = $doc
          ->getField($field_name);
      }

      // Copy code from comment_nodeapi().
      $extra[] = format_plural($doc->comment_count, '1 comment', '@count comments');
      $results[] = array(
        'link' => url($doc->path, NULL, NULL, 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,
        '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',
    );
  }

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

  // Get CCK field facets.
  $fields = apachesolr_cck_fields();
  if ($fields) {
    foreach ($fields as $name => $field) {

      // $delta can only be 32 chars, and the CCK field name may be this
      // long also, so we cannot add anything to it.
      $facets[$field['field_name']] = array_merge($field, array(
        'info' => t('CCK @field_type field: Filter by @field (@field_name)', array(
          '@field_type' => $field['field_type'],
          '@field' => $field['label'],
          '@field_name' => $field['field_name'],
        )),
        'facet_field' => apachesolr_index_key($field),
      ));
    }
  }
  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];
        }
      }
      $blocks['currentsearch'] = array(
        'info' => t('Apache Solr Search: Current search'),
      );
      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;
        }

        // 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;
                  return apachesolr_facet_block($response, $query, 'apachesolr_search', $delta, $index_key, t('Filter by @field', array(
                    '@field' => $field['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_get_vocabulary($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)) {

      // Get information needed by the taxonomy blocks about limits.
      $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
      $limit_default = variable_get('apachesolr_facet_query_initial_limit_default', 10);
      $limit = isset($initial_limits['apachesolr_search'][$delta]) ? $initial_limits['apachesolr_search'][$delta] : $limit_default;
      return array(
        'subject' => t('Filter by @name', array(
          '@name' => $vocab->name,
        )),
        'content' => theme('apachesolr_facet_list', $items, $limit),
      );
    }
  }
}

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

/**
 * 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) {
  $items = array();
  foreach ($facets as $field) {
    $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'];
    }
    $link = array();
    $new_query = clone $query;
    if (!empty($field['#active'])) {

      // '*' sorts before all numbers.
      $sortpre = '*';
      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 {
      $sortpre = 1000000 - $field['#count'];
      $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);
    }

    // We don't display children unless the parent is clicked.
    if (!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) {
    ksort($items);
  }
  return array_values($items);
}

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

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

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

  // 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']);
  }
  $keys = $form_values[$form_id . '_keys'];

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

/**
 * 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_id, $form_values) {
  $fv = $form_values;
  $keys = $fv['processed_keys'];
  $base = 'search/' . $fv['module'] . '/';
  if (variable_get('clean_url', '0')) {
    $keys = str_replace('+', '%2B', $keys);
  }
  $get = unserialize($fv['apachesolr_search']['get']);
  $queryvalues = unserialize($fv['apachesolr_search']['queryvalues']);
  if (!empty($fv['apachesolr_search']['retain-filters']) && $queryvalues) {
    $get = $queryvalues + $get;
    $get['retain-filters'] = '1';
  }
  if ($keys == '' && !$queryvalues) {
    form_set_error('keys', t('Please enter some keywords.'));
  }
  if (empty($get) || !is_array($get)) {
    return $base . $keys;
  }
  else {
    return array(
      $base . $keys,
      drupal_query_string_encode($get),
    );
  }
}

/**
 * Implementation of hook_form_alter().
 *
 * This adds options to the apachesolr admin form.
 */
function apachesolr_search_form_alter($form_id, &$form) {
  switch ($form_id) {
    case 'apachesolr_delete_index_form':
      if ($form_values['op'] == t('Delete the index')) {

        // In D6: $form['submit']['#submit'][] = 'apaechesolr_search_build_spellcheck';
        $form['#submit']['apachesolr_search_build_spellcheck'] = array();
      }
      break;
    case 'search_form':
      if ($form['module']['#value'] == 'apachesolr_search') {
        $form['#submit']['apachesolr_search_form_search_submit'] = array();

        // 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',
          '#default_value' => serialize($queryvalues),
        );
        $form['basic']['apachesolr_search']['get'] = array(
          '#type' => 'hidden',
          '#default_value' => serialize(array_diff_key($_GET, array(
            'q' => 1,
            'page' => 1,
            'filters' => 1,
            'solrsort' => 1,
            'retain-filters' => 1,
          ))),
        );
        if ($queryvalues || isset($_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', FALSE) && $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)),
                );
              }
            }
          }
        }
      }
      break;
    case 'search_block_form':
      if (variable_get('apachesolr_search_make_default', 0)) {
        if (!isset($form['#submit'])) {
          $form['#submit']['apachesolr_search_search_box_form_submit'] = array();
        }
        else {
          $key = isset($form['#submit']['search_box_form_submit']) ? 'search_box_form_submit' : FALSE;
          if ($key !== FALSE) {

            // Replace the search module's function.
            unset($form['#submit'][$key]);
            $form['#submit']['apachesolr_search_search_box_form_submit'] = array();
          }
        }
      }
      break;
    case 'search_theme_form':
      apachesolr_search_form_alter('search_block_form', $form);
      break;
    case 'apachesolr_settings':
      $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', 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', FALSE),
        '#description' => t('Enable spellchecker and get word suggestions. Also known as the "Did you mean ... ?" feature.'),
      );
      $form['#submit']['apachesolr_search_build_spellcheck'] = array();
      $form['#submit']['apachesolr_search_make_default_submit'] = array();

      // Move buttons to the bottom.
      $buttons = $form['buttons'];
      unset($form['buttons']);
      $form['buttons'] = $buttons;
      break;
    case 'search_admin_settings':
      $form['indexing_throttle']['search_cron_limit']['#options']['0'] = '0';
      ksort($form['indexing_throttle']['search_cron_limit']['#options']);
      break;
  }
}

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

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

    // Take account of path changes
    menu_rebuild();
  }
}
function apachesolr_search_build_spellcheck($form_id, $form_values) {
  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())), WATCHDOG_ERROR);
  }
}

/**
 * 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';
  }
}
function theme_apachesolr_breadcrumb_cck($field) {
  $matches = preg_split('/_cck_/', $field['#name']);
  if (isset($matches[1])) {
    $mappings = apachesolr_cck_fields();
    if (isset($mappings[$matches[1]]['display_callback'])) {
      $function = $mappings[$matches[1]]['display_callback'];
      if (function_exists($function)) {
        $facet = $field['#value'];
        $options = 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) {
  return implode(' ... ', $snippets) . ' ...';
}
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();
    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_process_response
apachesolr_search_add_boost_params
apachesolr_search_add_facet_params
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_build_spellcheck
apachesolr_search_collect_children Generate a list including the field and all its children.
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_form_alter Implementation of hook_form_alter().
apachesolr_search_form_search_submit Added form submit function to account for Apache mode_rewrite quirks.
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_nested_facet_items Recursive function that returns a nested array of facet values for use with theme_item_list().
apachesolr_search_noresults
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_update_index Implementation of hook_update_index().
apachesolr_search_view Re-implementation of search_view().
theme_apachesolr_breadcrumb_cck
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_currentsearch Return current search block contents
theme_apachesolr_search_snippets Theme the highlighted snippet text for a search entry.