You are here

Solr_Base_Query.php in Apache Solr Search 5.2

File

Solr_Base_Query.php
View source
<?php

class Solr_Base_Query implements Drupal_Solr_Query_Interface {

  /**
   * Extract all uses of one named field from a filter string e.g. 'type:book'
   */
  public function filter_extract(&$filterstring, $name) {
    $extracted = array();
    $name = preg_quote($name, '/');

    // Range queries.  The "TO" is case-sensitive.
    $patterns[] = '/(^| |-)' . $name . ':([\\[\\{](\\S+) TO (\\S+)[\\]\\}])/';

    // Match quoted values.
    $patterns[] = '/(^| |-)' . $name . ':"([^"]*)"/';

    // Match unquoted values.
    $patterns[] = '/(^| |-)' . $name . ':([^ ]*)/';
    foreach ($patterns as $p) {
      if (preg_match_all($p, $filterstring, $matches, PREG_SET_ORDER)) {

        // Sort matches longest to shortest to avoid accidentally
        // removing a sub-string.
        usort($matches, array(
          $this,
          'filter_extract_cmp',
        ));
        foreach ($matches as $match) {
          $filter = array();
          $filter['#query'] = $match[0];
          $filter['#exclude'] = $match[1] == '-';
          $filter['#value'] = trim($match[2]);

          // Empty values cause Lucene parse errors, so skip them.
          if (strlen($filter['#value'])) {
            if (isset($match[3])) {

              // Extra data for range queries
              $filter['#start'] = $match[3];
              $filter['#end'] = $match[4];
            }
            $extracted[] = $filter;
          }

          // Update the local copy of $filters by removing the match.
          $filterstring = str_replace($match[0], '', $filterstring);
        }
      }
    }
    return $extracted;
  }
  public function filter_extract_cmp($a, $b) {
    if (strlen($a[0]) == strlen($b[0])) {
      return 0;
    }
    return strlen($a[0]) > strlen($b[0]) ? -1 : 1;
  }

  /**
   * Takes an array $field and combines the #name and #value in a way
   * suitable for use in a Solr query.
   */
  public function make_filter(array $filter) {

    // If the field value has spaces, or : in it, wrap it in double quotes.
    // unless it is a range query.
    if (preg_match('/[ :]/', $filter['#value']) && !isset($filter['#start']) && !preg_match('/[\\[\\{]\\S+ TO \\S+[\\]\\}]/', $filter['#value'])) {
      $filter['#value'] = '"' . $filter['#value'] . '"';
    }
    $prefix = empty($filter['#exclude']) ? '' : '-';
    return $prefix . $filter['#name'] . ':' . $filter['#value'];
  }

  /**
   * Static shared by all instances, used to increment ID numbers.
   */
  protected static $idCount = 0;

  /**
   * Each query/subquery will have a unique ID
   */
  public $id;

  /**
   * A keyed array where the key is a position integer and the value
   * is an array with #name and #value properties.  Each value is a
   * used for filter queries, e.g. array('#name' => 'uid', '#value' => 0)
   * for anonymous content.
   */
  protected $fields = array();
  protected $fields_added = array();
  protected $fields_removed = array();

  /**
   * The complete filter string for a query.  Usually from $_GET['filters']
   * Contains name:value pairs for filter queries.  For example,
   * "type:book" for book nodes.
   */
  protected $filterstring;

  /**
   * A mapping of field names from the URL to real index field names.
   */
  protected $field_map = array();

  /**
   * An array of subqueries.
   */
  protected $subqueries = array();

  /**
   * The search keywords.
   */
  protected $keys;

  /**
   * The search base path.
   */
  protected $base_path;

  /**
   * Apache_Solr_Service object
   */
  protected $solr;
  protected $available_sorts;

  // Makes sure we always have a valid sort.
  protected $solrsort = array(
    '#name' => 'score',
    '#direction' => 'asc',
  );

  /**
   * @param $solr
   *   An instantiated Apache_Solr_Service Object.
   *   Can be instantiated from apachesolr_get_solr().
   *
   * @param $keys
   *   The string that a user would type into the search box. Suitable input
   *   may come from search_get_keys().
   *
   * @param $filterstring
   *   Key and value pairs that are applied as filter queries.
   *
   * @param $sortstring
   *   Visible string telling solr how to sort - added to GET query params.
   *
   * @param $base_path
   *   The search base path (without the keywords) for this query.
   */
  function __construct($solr, $keys, $filterstring, $sortstring, $base_path) {
    $this->solr = $solr;
    $this->keys = trim($keys);
    $this->filterstring = trim($filterstring);
    $this
      ->parse_filters();
    $this->available_sorts = $this
      ->default_sorts();
    $this->sortstring = trim($sortstring);
    $this
      ->parse_sortstring();
    $this->base_path = $base_path;
    $this->id = ++self::$idCount;
  }
  function __clone() {
    $this->id = ++self::$idCount;
  }
  public function add_filter($field, $value, $exclude = FALSE, $callbacks = array()) {
    $filter = array(
      '#exclude' => $exclude,
      '#name' => $field,
      '#value' => trim($value),
      '#callbacks' => $callbacks,
    );
    $this->fields[] = $filter;
    $this->fields_added[] = $filter;
  }

  /**
   * Get all filters, or the subset of filters for one field.
   *
   * @param $name
   *   Optional name of a Solr field.
   */
  public function get_filters($name = NULL) {
    if (empty($name)) {
      return $this->fields;
    }
    reset($this->fields);
    $matches = array();
    foreach ($this->fields as $filter) {
      if ($filter['#name'] == $name) {
        $matches[] = $filter;
      }
    }
    return $matches;
  }
  public function remove_filter($name, $value = NULL) {

    // We can only remove named fields.
    if (empty($name)) {
      return;
    }

    // Record the removal.
    $this->fields_removed[$name][] = $value;

    // Remove from the public list of filters.
    $this
      ->unset_filter($this->fields, $name, $value);

    // Remove from the list of filters added with add_filter()
    $this
      ->unset_filter($this->fields_added, $name, $value);
  }
  protected function unset_filter(&$fields, $name, $value) {
    if (!isset($value)) {
      foreach ($fields as $pos => $values) {
        if ($values['#name'] == $name) {
          unset($fields[$pos]);
        }
      }
    }
    else {
      foreach ($fields as $pos => $values) {
        if ($values['#name'] == $name && $values['#value'] == $value) {
          unset($fields[$pos]);
        }
      }
    }
  }
  public function has_filter($name, $value) {
    foreach ($this->fields as $pos => $values) {
      if (isset($values['#name']) && isset($values['#value']) && $values['#name'] == $name && $values['#value'] == $value) {
        return TRUE;
      }
    }
    return FALSE;
  }

  /**
   * Handle aliases for field to make nicer URLs
   *
   * @param $field_map
   *   An array keyed with real Solr index field names, with value being the alias.
   */
  function add_field_aliases($field_map) {
    $this->field_map = array_merge($this->field_map, $field_map);

    // We have to re-parse the filters.
    $this
      ->parse_filters();
  }
  function get_field_aliases() {
    return $this->field_map;
  }
  function clear_field_aliases() {
    $this->field_map = array();

    // We have to re-parse the filters.
    $this
      ->parse_filters();
  }

  /**
   * A subquery is another instance of a Solr_Base_Query that should be joined
   * to the query. The operator determines whether it will be joined with AND or
   * OR.
   *
   * @param $query
   *   An instance of Drupal_Solr_Query_Interface.
   *
   * @param $operator
   *   'AND' or 'OR'
   */
  public function add_subquery(Drupal_Solr_Query_Interface $query, $fq_operator = 'OR', $q_operator = 'AND') {
    $this->subqueries[$query->id] = array(
      '#query' => $query,
      '#fq_operator' => $fq_operator,
      '#q_operator' => $q_operator,
    );
  }
  public function remove_subquery(Drupal_Solr_Query_Interface $query) {
    unset($this->subqueries[$query->id]);
  }
  public function remove_subqueries() {
    $this->subqueries = array();
  }
  protected function parse_sortstring() {

    // Substitute any field aliases with real field names.
    $sortstring = strtr($this->sortstring, array_flip($this->field_map));

    // Score is a special case - it's the default sort for Solr.
    if ('' == $sortstring) {
      $this
        ->set_solrsort('score', 'asc');
    }
    else {

      // Validate and set sort parameter
      $fields = implode('|', array_keys($this->available_sorts));
      if (preg_match('/^(?:(' . $fields . ') (asc|desc),?)+$/', $sortstring, $matches)) {

        // We only use the last match.
        $this
          ->set_solrsort($matches[1], $matches[2]);
      }
    }
  }
  public function set_solrsort($name, $direction) {
    if (isset($this->available_sorts[$name])) {
      $this->solrsort = array(
        '#name' => $name,
        '#direction' => $direction,
      );
    }
  }
  public function get_solrsort() {
    return $this->solrsort;
  }
  public function get_available_sorts() {
    return $this->available_sorts;
  }
  public function set_available_sort($name, $sort) {

    // We expect non-aliased sorts to be added.
    $this->available_sorts[$name] = $sort;

    // Re-parse the sortstring.
    $this
      ->parse_sortstring();
  }
  public function remove_available_sort($name) {
    unset($this->available_sorts[$name]);

    // Re-parse the sortstring.
    $this
      ->parse_sortstring();
  }

  /**
   * Returns a default list of sorts.
   */
  protected function default_sorts() {

    // The array keys must always be real Solr index fields.
    return array(
      'score' => array(
        'title' => t('Relevancy'),
        'default' => 'asc',
      ),
      'sort_title' => array(
        'title' => t('Title'),
        'default' => 'asc',
      ),
      'type' => array(
        'title' => t('Type'),
        'default' => 'asc',
      ),
      'sort_name' => array(
        'title' => t('Author'),
        'default' => 'asc',
      ),
      'created' => array(
        'title' => t('Date'),
        'default' => 'desc',
      ),
    );
  }

  /**
   * Return filters and sort in a form suitable for a query param to url().
   */
  public function get_url_queryvalues() {
    $queryvalues = array();
    if ($fq = $this
      ->rebuild_fq(TRUE)) {
      $queryvalues['filters'] = implode(' ', $fq);
    }
    $solrsort = $this->solrsort;
    if ($solrsort && ($solrsort['#name'] != 'score' || $solrsort['#direction'] != 'asc')) {
      if (isset($this->field_map[$solrsort['#name']])) {
        $solrsort['#name'] = $this->field_map[$solrsort['#name']];
      }
      $queryvalues['solrsort'] = $solrsort['#name'] . ' ' . $solrsort['#direction'];
    }
    return $queryvalues;
  }
  public function get_fq() {
    return $this
      ->rebuild_fq();
  }

  /**
   * A function to get just the keyword components of the query,
   * omitting any field:value portions.
   */
  public function get_query_basic() {
    return $this
      ->rebuild_query();
  }

  /**
   * Return the search path.
   *
   * @param string $new_keywords
   *   Optional. When set, this string overrides the query's current keywords.
   */
  public function get_path($new_keywords = NULL) {
    if (isset($new_keywords)) {
      return $this->base_path . '/' . $new_keywords;
    }
    return $this->base_path . '/' . $this
      ->get_query_basic();
  }

  /**
   * Build additional breadcrumb elements relative to $base.
   */
  public function get_breadcrumb($base = NULL) {
    $progressive_crumb = array();
    if (!isset($base)) {
      $base = $this
        ->get_path();
    }
    $search_keys = $this
      ->get_query_basic();
    if ($search_keys) {
      $breadcrumb[] = l($search_keys, $base);
    }
    foreach ($this->fields as $field) {
      $name = $field['#name'];

      // Look for a field alias.
      if (isset($this->field_map[$name])) {
        $field['#name'] = $this->field_map[$name];
      }
      $progressive_crumb[] = $this
        ->make_filter($field);
      $options_query = 'filters=' . rawurlencode(implode(' ', $progressive_crumb));
      $breadcrumb_name = 'apachesolr_breadcrumb_' . $name;

      // Modules utilize this alter to consolidate several fields into one
      // theme function. This is how CCK breadcrumbs are handled.
      drupal_alter('apachesolr_theme_breadcrumb', $breadcrumb_name);
      if ($themed = theme($breadcrumb_name, $field)) {
        $breadcrumb[] = l($themed, $base, array(), $options_query);
      }
      else {
        $breadcrumb[] = l($field['#value'], $base, array(), $options_query);
      }
    }

    // The last breadcrumb is the current page, so it shouldn't be a link.
    $last = count($breadcrumb) - 1;
    $breadcrumb[$last] = strip_tags($breadcrumb[$last]);
    return $breadcrumb;
  }

  /**
   * Parse the filter string in $this->filters into $this->fields.
   *
   * Builds an array of field name/value pairs.
   */
  protected function parse_filters() {
    $this->fields = array();
    $parsed_fields = array();
    $filterstring = $this->filterstring;

    // Gets information about the fields already in solr index.
    $index_fields = $this->solr
      ->getFields();
    foreach ((array) $index_fields as $name => $data) {

      // Look for a field alias.
      $alias = isset($this->field_map[$name]) ? $this->field_map[$name] : $name;

      // Get the values for $name
      $extracted = $this
        ->filter_extract($filterstring, $alias);
      if (count($extracted)) {
        foreach ($extracted as $filter) {
          $pos = strpos($this->filterstring, $filter['#query']);

          // $solr_keys and $solr_crumbs are keyed on $pos so that query order
          // is maintained. This is important for breadcrumbs.
          $filter['#name'] = $name;
          $parsed_fields[$pos] = $filter;
        }
      }
    }

    // Even though the array has the right keys they are likely in the wrong
    // order. ksort() sorts the array by key while maintaining the key.
    ksort($parsed_fields);
    foreach ($this->fields_removed as $name => $values) {
      foreach ($values as $val) {
        $this
          ->unset_filter($parsed_fields, $name, $val);
      }
    }
    $this->fields = array_merge(array_values($parsed_fields), $this->fields_added);
  }

  /**
   * Builds a set of filter queries from $this->fields and all subqueries.
   *
   * Returns an array of strings that can be combined into
   * a URL query parameter or passed to Solr as fq paramters.
   */
  protected function rebuild_fq($aliases = FALSE) {
    $fq = array();
    $fields = array();
    foreach ($this->fields as $pos => $field) {

      // Look for a field alias.
      if ($aliases && isset($this->field_map[$field['#name']])) {
        $field['#name'] = $this->field_map[$field['#name']];
      }
      $fq[] = $this
        ->make_filter($field);
    }
    foreach ($this->subqueries as $id => $data) {
      $subfq = $data['#query']
        ->rebuild_fq($aliases);
      if ($subfq) {
        $operator = $data['#fq_operator'];
        $fq[] = "(" . implode(" {$operator} ", $subfq) . ")";
      }
    }
    return $fq;
  }
  protected function rebuild_query() {
    $query = $this->keys;
    foreach ($this->subqueries as $id => $data) {
      $operator = $data['#q_operator'];
      $subquery = $data['#query']
        ->get_query_basic();
      if ($subquery) {
        $query .= " {$operator} ({$subquery})";
      }
    }
    return $query;
  }

}

Classes

Namesort descending Description
Solr_Base_Query