You are here

apachesolr_date.module in Apache Solr Search 6.2

Integration with the Apache Solr search application. Provides faceting for CCK Date fields.

File

contrib/apachesolr_date/apachesolr_date.module
View source
<?php

/**
 * @file
 *   Integration with the Apache Solr search application. Provides
 *   faceting for CCK Date fields.
 */

/**
 * Implementation of hook_apachesolr_facets().
 * Only handles end date facets.
 * Start date facet definitions are handled later.
 * @see apachesolr_date_apachesolr_cck_fields_alter()
 */
function apachesolr_date_apachesolr_facets() {

  // Get CCK field facets.
  $fields = apachesolr_cck_fields();
  $facets = array();
  if ($fields) {
    foreach ($fields as $name => $field) {
      if (in_array($field['field_type'], array(
        'date',
        'datetime',
        'datestamp',
      ))) {
        $cck_info = content_fields($field['field_name']);

        // Only fields with a value2 have end dates to facet upon.
        if (isset($cck_info['columns']['value2'])) {

          // $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'] . '_end'] = array_merge($field, array(
            'info' => t('CCK @field_type field: Filter by @field end', array(
              '@field_type' => $field['field_type'],
              '@field' => $field['label'],
            )),
            // 'facet_field' will later be block deltas.
            'facet_field' => apachesolr_index_key($field) . '_end',
            'content_types' => $field['content_types'],
            'display_callback' => 'apachesolr_date_display_callback',
          ));
        }
      }
    }
  }
  return $facets;
}

/**
 * Implementation of hook_block().
 */
function apachesolr_date_block($op = 'list', $delta = 0, $edit = array()) {
  switch ($op) {
    case 'list':
      $enabled_facets = apachesolr_get_enabled_facets('apachesolr_date');
      $facets = apachesolr_date_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,
          );

          // TODO: This is ugly. The only consolation is that the admin can
          // override block titles anyway.
          $blocks[$delta]['label'] = $blocks[$delta]['label'] . ' (end)';
        }
      }
      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_date');
        if (empty($facets[$delta])) {
          return;
        }
        if (!apachesolr_block_visibility($query, 'apachesolr_date', $delta)) {
          return;
        }

        // $delta comes to us with _end on the end, which we snip off right here.
        $delta = substr($delta, 0, strlen($delta) - 4);
        if ($fields = apachesolr_cck_fields()) {
          if ($field = $fields[$delta]) {
            $index_key = apachesolr_index_key($field) . '_end';
            $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 end', array(
              '@field' => $field['label'],
            )), $callback);
          }
        }
      }
      break;
    case 'configure':
      return apachesolr_facetcount_form('apachesolr_date', $delta);
    case 'save':
      apachesolr_facetcount_save($edit);
      break;
  }
}

/**
 * Implementation of hook_apachesolr_prepare_query().
 * Adds sorts for enabled date facets to the sorting block.
 */
function apachesolr_date_apachesolr_prepare_query(&$query) {

  // Because we get the enabled facets just from apachesolr_search we're
  // limiting sorting to start dates.
  $enabled_facets = apachesolr_get_enabled_facets('apachesolr_search');
  $facet_definitions = apachesolr_date_apachesolr_facets();
  $facet_definitions += apachesolr_search_apachesolr_facets();
  foreach ($enabled_facets as $key => $value) {
    if (strpos($value, 'tds_cck_field_date') !== FALSE) {
      $cck_field = content_fields($facet_definitions[$key]['field_name']);
      $query
        ->set_available_sort($value, array(
        'title' => $cck_field['widget']['label'],
        // how the sort is to appear in the sorts block
        'default' => 'desc',
      ));
    }
  }
}

/**
 * Implementation of hook_apachesolr_cck_fields_alter().
 * This function adds the CCK date fields' definitions to let
 * them be recognized as facets.
 */
function apachesolr_date_apachesolr_cck_fields_alter(&$mappings) {
  $defaults = array(
    'indexing_callback' => 'apachesolr_date_date_field_indexing_callback',
    // Trie-Range date types.
    'index_type' => 'tdate',
    'facet_block_callback' => 'apachesolr_date_date_facet_block',
    'display_callback' => 'apachesolr_date_display_callback',
    'facets' => TRUE,
  );

  // NOTE: The structure of this array essentially blocks us from having
  // multiple mappings per CCK field. For that we'd need a structure like
  // $mappings['date']['date_select'][]      = $defaults;
  $mappings['date']['date_select'] = $defaults;
  $mappings['date']['date_text'] = $defaults;
  $mappings['datetime']['date_select'] = $defaults;
  $mappings['datetime']['date_text'] = $defaults;
  $mappings['datestamp']['date_select'] = $defaults;
  $mappings['datestamp']['date_text'] = $defaults;
  $mappings['datestamp']['date_select']['indexing_callback'] = 'apachesolr_date_datestamp_field_indexing_callback';
  $mappings['datestamp']['date_text']['indexing_callback'] = 'apachesolr_date_datestamp_field_indexing_callback';
  if (module_exists('date_popup')) {
    $mappings['date']['date_popup'] = $defaults;
    $mappings['datetime']['date_popup'] = $defaults;
    $mappings['datestamp']['date_popup'] = $defaults;
    $mappings['datestamp']['date_popup']['indexing_callback'] = 'apachesolr_date_datestamp_field_indexing_callback';
  }
}

/**
 * This function is used during indexing to normalize the DATE and DATETIME
 * fields into the appropriate format for Apache Solr.
 */
function apachesolr_date_date_field_indexing_callback($node, $field_name, $cck_info) {
  $fields = array();
  if (isset($node->{$field_name})) {
    $index_key = apachesolr_index_key($cck_info);
    foreach ($node->{$field_name} as $field) {

      // Construct a Solr-ready date string in UTC time zone based on the field's date string and time zone.
      $tz = new DateTimeZone(isset($field['timezone']) ? $field['timezone'] : 'UTC');

      // $fields may end up having two values; one for the start date
      // and one for the end date.
      if (isset($field['value'])) {
        if ($date = date_create($field['value'], $tz)) {
          $index_value = apachesolr_date_iso($date
            ->format('U'));
          $fields[] = array(
            'key' => $index_key,
            'value' => $index_value,
          );
        }
      }
      if (isset($field['value2'])) {
        if ($date = date_create($field['value2'], $tz)) {
          $index_value = apachesolr_date_iso($date
            ->format('U'));
          $fields[] = array(
            // The value2 element is the end date. Therefore it gets indexed
            // into its own Solr field.
            'key' => $index_key . '_end',
            'value' => $index_value,
          );
        }
      }
    }
  }
  return $fields;
}

/**
 * This function is used during indexing to normalize the DATESTAMP fields
 * into the appropriate format for Apache Solr.
 */
function apachesolr_date_datestamp_field_indexing_callback($node, $field_name, $cck_info) {
  $fields = array();
  if (isset($node->{$field_name})) {
    $index_key = apachesolr_index_key($cck_info);

    // $fields may end up having two values; one for the start date
    // and one for the end date.
    foreach ($node->{$field_name} as $field) {
      if (isset($field['value']) && $field['value'] != 0) {
        $index_value = apachesolr_date_iso($field['value']);
        $fields[] = array(
          'key' => $index_key,
          'value' => $index_value,
        );
      }
      if (isset($field['value2']) && $field['value'] != 0) {
        $index_value = apachesolr_date_iso($field['value2']);
        $fields[] = array(
          // The value2 element is the end date. Therefore it gets indexed
          // into its own Solr field.
          'key' => $index_key . '_end',
          'value' => $index_value,
        );
      }
    }
  }
  return $fields;
}

/**
 * When faceting and filtering we need to infer ranges of dates.
 * This function looks at a query and a facet field and returns a
 * date range for use in querying.
 *
 * @param Drupal_Solr_Query_Interface $query
 *   Current query object.
 * @param $facet_field
 *   The field for which a range must be generated.
 *
 * @return array $gap
 *   The array contains a start, end, and gap element.
 *
 *  Example return value:
 *  array(
 *    0 => '2007-01-05T13:05:00Z/YEAR',
 *    1 => '2013-11-17T15:04:00Z+1YEAR/YEAR',
 *    2 => '+1YEAR',
 *  );
 *
 */
function apachesolr_date_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 the query object, get initial
  // facet.date.* params from the DB and determine the best search
  // gap to use.
  if (!isset($start_iso)) {

    // NOTE: Finding the field namd and loading the field info is a hacky
    // bit of string manipulation. We look once with what comes in ($field_name),
    // and if that doesn't find any CCK field definition for us, we hack off
    // the last four characters (presumed to be '_end' for an ending date),
    // and try again. If that doesn't find anything we go home.
    $field_name = substr($facet_field, 8);
    $field = content_fields($field_name);
    $db_info = content_database_info($field);
    $column = $db_info['columns']['value']['column'];

    // This check is in place for the cases where the field name has
    // _end appended to the end, and signifies that it is and end date.
    if (!$field) {
      $field_name = substr($field_name, 0, strlen($field_name) - 4);
      $field = content_fields($field_name);
      $db_info = content_database_info($field);
      $column = $db_info['columns']['value2']['column'];
    }

    // By this point we should have the following:
    //   $field_name, a cck field name
    //   $field, a cck field definition
    //   $db_info, information from content.module about retrieving db data
    //   $column, the column name in the db table for this field
    if (!$field) {
      return;
    }
    if (!empty($field['timezone_db'])) {
      $tz = new DateTimeZone($field['timezone_db']);
    }
    else {

      // The commented code takes the TZ from the computer the site is on.

      //$tz = new DateTimeZone(date_default_timezone_get());
      $tz = new DateTimeZone('UTC');
    }
    $table = $db_info['table'];
    $start_value = db_result(db_query("SELECT MIN(cck.{$column}) FROM {{$table}} cck INNER JOIN {node} n ON cck.vid = n.vid WHERE n.status = 1"));
    if (is_numeric($start_value)) {
      $start_iso = apachesolr_date_iso($start_value);
    }
    elseif ($date = date_create($start_value, $tz)) {
      $start_iso = apachesolr_date_iso($date
        ->format('U'));
    }

    // Subtract one second, so that this range's $end_iso is not equal to the
    // next range's $start_iso.
    $end_value = db_result(db_query("SELECT MAX(cck.{$column}) FROM {{$table}} cck INNER JOIN {node} n ON cck.vid = n.vid WHERE n.status = 1"));
    if (is_numeric($end_value)) {
      $end_iso = apachesolr_date_iso($end_value - 1);
    }
    elseif ($date = date_create($end_value, $tz)) {
      $end_iso = apachesolr_date_iso($date
        ->format('U') - 1);
    }
    if (isset($start_iso) && isset($end_iso)) {
      $gap = apachesolr_date_determine_gap($start_iso, $end_iso);
    }
    else {

      // TODO: Find the gap.
      $end_iso = $start_iso;
      $gap = "YEAR";
    }
  }

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

/**
 * Helper function for displaying a date facet blocks.
 */
function apachesolr_date_date_facet_block($response, $query, $module, $delta, $facet_field, $filter_by, $facet_callback = FALSE) {

  // The items in the facet block (facet links and unclick links).
  $items = array();

  // The array that is ultimately sent into apachesolr_l(), @see apachesolr_l()
  $options = array();

  // The display text for any given facet or unclick link.
  $facet_text = '';

  // Clone the query as we either add or remove a filter before generating
  // the link.
  $new_query = clone $query;

  // Set the default gap.
  $gap = 'YEAR';
  foreach (array_reverse($new_query
    ->get_filters($facet_field)) as $filter) {
    $new_query
      ->remove_filter($facet_field, $filter['#value']);
    $gap = apachesolr_date_find_query_gap($filter['#start'], $filter['#end']);
    $facet_text = apachesolr_date_format_iso_by_gap($gap, $filter['#start']);
    $options['gap'] = $gap;
    $options['query'] = $new_query
      ->get_url_queryvalues();
    array_unshift($items, theme('apachesolr_unclick_link', $facet_text, $new_query
      ->get_path(), $options));
  }

  // Add links for additional date filters.
  // NOTE: Date fields come in $response->facet_counts->facet_fields
  // but others come in $response->facet_counts->facet_dates.
  if (!empty($response->facet_counts->facet_dates->{$facet_field})) {
    $field = clone $response->facet_counts->facet_dates->{$facet_field};
  }
  elseif (!empty($response->facet_counts->facet_fields->{$facet_field})) {
    $field = clone $response->facet_counts->facet_fields->{$facet_field};
  }
  if ($field) {

    // A field will have this type of structure, where the main information
    // is in the form DATE => COUNT:
    // stdClass Object
    // (
    //    [2007-01-01T00:00:00Z] => 5
    //    [2008-01-01T00:00:00Z] => 4
    //    [2009-01-01T00:00:00Z] => 2
    //    [2010-01-01T00:00:00Z] => 6
    //    [2011-01-01T00:00:00Z] => 7
    //    [2012-01-01T00:00:00Z] => 4
    //    [2013-01-01T00:00:00Z] => 4
    //    [gap] => +1YEAR
    //    [end] => 2014-01-01T00:00:00Z
    // )
    // Isolate $end and $gap and clean the field to only have the actual
    // date values.
    $end = $field->end;
    unset($field->end);
    if (isset($field->gap)) {
      $gap = $field->gap;
      unset($field->gap);
    }

    // Treat each date facet as a range start, and use the next date
    // facet as range end.  Use 'end' for the final end.
    $range_end = array();
    foreach ($field as $facet => $count) {
      if (isset($prev_facet)) {
        $range_end[$prev_facet] = $facet;
      }
      $prev_facet = $facet;
    }
    $range_end[$prev_facet] = $end;
    foreach ($field as $facet => $count) {

      // Solr sends this back if it's empty.
      if ($facet == '_empty_' || $count == 0) {
        continue;
      }
      if ($facet_callback && function_exists($facet_callback)) {
        $facet_text = $facet_callback($facet, array(
          'gap' => $gap,
        ));
      }
      else {
        $facet_text = apachesolr_date_format_iso_by_gap($gap, $facet);
      }
      $new_query = clone $query;
      $new_query
        ->add_filter($facet_field, '[' . $facet . ' TO ' . $range_end[$facet] . ']');
      $options['query'] = $new_query
        ->get_url_queryvalues();
      $items[] = theme('apachesolr_facet_link', $facet_text, $new_query
        ->get_path(), $options, $count, FALSE, $response->response->numFound);
    }
  }
  if (count($items) > 0) {

    // Get information needed by the rest of the blocks about limits.
    $initial_limits = variable_get('apachesolr_facet_query_initial_limits', array());
    $limit = isset($initial_limits[$module][$delta]) ? $initial_limits[$module][$delta] : variable_get('apachesolr_facet_query_initial_limit_default', 10);
    $output = theme('apachesolr_facet_list', $items, $limit, $delta);
    return array(
      'subject' => $filter_by,
      'content' => $output,
    );
  }
  return NULL;
}

/**
 *  Handles facet text and breadcrumb displays for dates or date ranges.
 *  @param $facet
 *    A date string, eg: 2008-01-01T00:00:00Z or a date range, eg: [2007-01-01T00:00:00Z TO 2008-01-01T00:00:00Z]
 *  @param $options
 *    An array with a key 'gap' of the form +1YEAR, or +1MONTH etc.
 *
 *  TODO: Why is $options an array?
 */
function apachesolr_date_display_callback($facet, $options) {
  if (preg_match('@[\\[\\{](\\S+) TO (\\S+)[\\]\\}]@', $facet, $match)) {
    return apachesolr_date_format_range($match[1], $match[2]);
  }
  $gap = preg_replace('/^\\+[0-9]+/', '', $options['gap']);
  return apachesolr_date_format_iso_by_gap($gap, $facet);
}

Functions

Namesort descending Description
apachesolr_date_apachesolr_cck_fields_alter Implementation of hook_apachesolr_cck_fields_alter(). This function adds the CCK date fields' definitions to let them be recognized as facets.
apachesolr_date_apachesolr_facets Implementation of hook_apachesolr_facets(). Only handles end date facets. Start date facet definitions are handled later.
apachesolr_date_apachesolr_prepare_query Implementation of hook_apachesolr_prepare_query(). Adds sorts for enabled date facets to the sorting block.
apachesolr_date_block Implementation of hook_block().
apachesolr_date_datestamp_field_indexing_callback This function is used during indexing to normalize the DATESTAMP fields into the appropriate format for Apache Solr.
apachesolr_date_date_facet_block Helper function for displaying a date facet blocks.
apachesolr_date_date_field_indexing_callback This function is used during indexing to normalize the DATE and DATETIME fields into the appropriate format for Apache Solr.
apachesolr_date_display_callback Handles facet text and breadcrumb displays for dates or date ranges.
apachesolr_date_search_date_range When faceting and filtering we need to infer ranges of dates. This function looks at a query and a facet field and returns a date range for use in querying.