. */ /** * Solr params representation. */ class AssistantSolrParams { /** * Key/value array of params to give to Solr. If a key have more than one * value, put an array of values as value. */ protected $_params = NULL; /** * Get Solr params formated array. * * @return array * Solr params formated for 'apachesolr' module. */ public function getParams() { return $this->_params; } /** * Remove a Solr param. * * @param string $key * Param name to remove * @param string $value * (optional) Solr param value to be removed. If null given, remove all * params matching $key. */ public function removeParam($key, $value = NULL) { if (! $value) { unset($this->_params[$key]); } else { if (is_array($this->_params[$key])) { foreach ($this->_params[$key] as $_key => $_value) { if ($_value == $value) { unset($this->_params[$_key]); } } } else if($this->_params[$key] == $value) { unset($this->_param[$key]); } } } /** * Add a Solr param to our query * * @param string $key * Solr param name * @param array|string $value * Solr param value, likely more a string any other type. If an array is * given, this will add a set of values for the given param name. */ public function addParam($key, $value) { if (isset($this->_params[$key])) { if (is_array($value)) { // array_values() will ensure we have numeric keys and ensure the // array_merge() expected behavior. if (is_array($this->_params[$key])) { $this->_params = array_merge($this->_params[$key], array_values($value)); } else { $this->_params = array_merge(array($this->_params[$key]), array_values($value)); } } else { if (is_array($this->_params[$key])) { $this->_params[$key][] = $value; } else { $this->_params[$key] = array($this->_params[$key], $value); } } } else { // Whatever is a single value or an array, this will do the right thing. $this->_params[$key] = $value; } } /** * Merge params array with existing one. * * @param array $params = array() * New params to add * @param boolean $override = FALSE * If set to TRUE, incomming params key will override existing ones */ public function addParams($params = array(), $override = FALSE) { foreach ($params as $key => &$value) { if ($override) { $this->removeParam($key); } $this->addParam($key, $value); } } /** * Construct the object * * @param array $params = array() * Apache Solr module formated array of params */ public function __construct($params = array()) { $this->_params = $params; } } /** * Error when building a wrong query. */ class SolrQueryException extends Exception {} /** * Static container for common static methods */ class AssistantSolrHelper { const OPERATOR_AND = 'AND'; const OPERATOR_OR = 'OR'; /** * Regex that matches all Lucene query syntax reserved chars */ private static $_reSpecials = '/\+-&\|!\(\)\{\}\[\]\^"~\*\?:\\\/'; /** * Add '"' chars if necessary to a token value, and escape Lucene query * syntax reserved chars. * * @param string $token * String to escape * @param boolean $force = FALSE * Escape whatever happens * * @return string */ public static function escapeToken($token, $force = FALSE) { $token = preg_replace(self::$_reSpecials, '\\\\\\0', $token); if ($force || preg_match(self::$_reSpecials, $token)) { return '"' . $token . '"'; } else { return $token; } } } class AssistantSolrTerm { const OPERATOR_REQUIRE = '+'; const OPERATOR_PROHIBIT = '-'; const OPERATOR_BOOST = '^'; /** * Convert given boost factor to some incredibly arbitrary value. */ public static function boostFactor($boost) { $boost = (float) $boost; // Consider that > 1 values are given to simulate a negative boost // If boost is lower than 100, apply our boost huge factor method. if ($boost < 100 && $boost > 1) { // Quite ugly, but larger values gives stuff ultra negative answers for // all non boosted terms. $boost *= 10; } return $boost; } /** * Escape and apply boost to given token * * @return string */ public function __toString() { $escaped = AssistantSolrHelper::escapeToken($this->_term, !empty($this->_boost)); if (!empty($this->_exclusion)) { return $this->_exclusion . $escaped; } else if (!empty($this->_boost)) { return $escaped . '^' . self::boostFactor($this->_boost); } else { return $escaped; } } /** * REQUIRE or PROHIBIT operator */ protected $_exclusion = NULL; /** * Set exclusion mode. * * @param string $exclusive = NULL * AssistantSolrTerm::OPERATOR_REQUIRE * or AssistantSolrTerm::OPERATOR_PROHIBIT * or NULL * * @throws SolrQueryException * In case of errors */ public function setExclusion($exclusion) { if (!empty($exclusion) && $exclusion != self::OPERATOR_PROHIBIT && $exclusion != self::OPERATOR_REQUIRE) { throw new SolrQueryException("Exclusion must be AssistantSolrTerm::OPERATOR_REQUIRE or AssistantSolrTerm::OPERATOR_PROHIBIT"); } $this->_exclusion = $exclusion; } /** * Boost float value, NULL for default */ protected $_boost = NULL; /** * Set exclusion mode. * * @param float $boost = NULL * (optional) Float superior to 0 or NULL * * @throws SolrQueryException * In case of errors */ public function setBoost($boost) { if (!empty($boost) && $boost <= 0) { throw new SolrQueryException("Boost must be a absolute positive float, value is : " . print_r($boost, TRUE)); } $this->_boost = (float) $boost; } /** * Term */ protected $_term = NULL; /** * Construct new term * * @param string $term * Term or value to lookup * @param float $boost = NULL * (optional) Float superior to 0 * @param string $exclusive = NULL * (optional) AssistantSolrTerm::OPERATOR_REQUIRE * or AssistantSolrTerm::OPERATOR_PROHIBIT * * @throws SolrQueryException * In case of errors */ public function __construct($term, $boost = NULL, $exclusion = NULL) { $this->_term = trim($term); $this->setExclusion($exclusion); $this->setBoost($boost); } } /** * Abstract implementation of complex query tree. */ abstract class AbstractAssistantSolrQueryString { /** * Sub queries storage. * * @var array */ protected $_subQueries = array(); /** * Default operator. Can be 'AND' or 'OR'. * * @var string */ protected $_operator = AssistantSolrHelper::OPERATOR_OR; /** * Set default operator. * * @param string $operator * Can be 'AND' or 'OR'. */ public function setOperator($operator) { if ($operator == AssistantSolrHelper::OPERATOR_AND || $operator == AssistantSolrHelper::OPERATOR_OR) { $this->_operator = $operator; } } /** * Add subquery to query * * @param AbstractAssistantSolrQueryString $subQuery * AbstractAssistantSolrQueryString instance */ public function addSubQuery(AbstractAssistantSolrQueryString $query) { $this->_subQueries[] = $query; } /** * Build RAW output (speciliazed method). * * @return string */ protected abstract function _renderRawOutput(); /** * Build RAW query string * * @return string */ public final function __toString() { $output = trim($this->_renderRawOutput()); if (! empty($this->_subQueries)) { $subOutput = implode(' ' . $this->_operator . ' ', $this->_subQueries); if (! empty($output)) { $output .= ' ' . $this->_operator . ' (' . $subOutput . ')'; } else { $output .= $subOutput; } } return $output; } } /** * Query string implementation. */ class AssistantSolrQueryString extends AbstractAssistantSolrQueryString { /** * Key/value pairs, keys are field string on which to apply the search token * values are the search token. * Key can be a numeric value, in this case it will be ignored and the value * will be apply as a default search pattern. * This will be used to construct the Solr Query string. */ protected $_q = array(); /** * Add query keyword to given query array. * * For our API, voost and not are exclusive, if not is present, boost will be * ignored. * * @param string|AssistantSolrTerm $value * If a string, a word or a phrase, every chars will be escaped. * If you want to use boost or require or prohibit operators, use a * AssistantSolrTerm instance. * @param string $field = NULL * (optional) Field name where to append the query string * Note that if this field already exists, string will be concatenated to * existing one. * @param int $boost = NULL * (optional) This will apply the boost to the given value * @param boolean $not = FALSE * (optional) If set to TRUE, will use the '-' operator to exclude documents * matching this statement. */ public function addQuery($value, $field = NULL) { $boost = ($not) ? NULL : (int) $boost; if (empty($field)) { $this->_q[] = $value; } else { if (! isset($this->_q[$field])) { $this->_q[$field] = array(); } $this->_q[$field][] = $value; } } /** * @override _renderRawOutput() * @see AbstractAssistantSolrQueryString#_renderRawOutput() */ protected function _renderRawOutput() { if (! empty($this->_q)) { $queryString = array(); foreach ($this->_q as $field => &$value) { // What our substring will be $subString = ""; // Skip numeric keys, and append the value as default search keyword if (! is_numeric($field)) { $values = array(); foreach ($value as $value_) { if ($value_ instanceof AssistantSolrTerm) { $values[] = (string) $value_; } else { $values[] = AssistantSolrHelper::escapeToken($value_); } } $subString .= $field . ':(' . implode(' ', $values) . ')'; } else { if ($value instanceof AssistantSolrTerm) { $subString .= (string) $value; } else { $subString .= AssistantSolrHelper::escapeToken($value); } } $queryString[] = $subString; } return implode(" ", $queryString); } return ""; } } /** * Query filter simple implementation. */ class AssistantSolrQueryFilter { /** * Key/value pairs, keys are field string on which to apply the search token * values are the boost value. */ protected $_qf = array(); /** * Add boost value to field * * @param string $field * Field name to boose * @param int $value * Boost value */ public function addBoost($field, $value) { if (!empty($value) && $value <= 0) { throw new SolrQueryException("Boost must be a absolute positive float, value is : " . print_r($value, TRUE)); } $this->_qf[$field] = (float) $value; } /** * Return string representation of this object (valid value for Solr engine * to parse). */ public function __toString() { if (! empty($this->_qf)) { $subStrings = array(); foreach ($this->_qf as $field => $value) { $subStrings = $field . '^' . $value; } return implode(' ', $substrings); } return ""; } } /** * Because Solr_Base_Query does not allows us to do full Solr queries, we done * our own simple implementation that fits to our needs. * * TODO: Cache the computed query strings until modifications are done. */ class AssistantSolrQuery { /** * Solr params array */ protected $_params = NULL; /** * Solr Query * * @var array * Array of AssistantSolrQueryString */ protected $_q = NULL; /* * Solr Query Filter * * @var array * Array of AssistantSolrQueryString */ protected $_fq = NULL; /* * Solr Filter query * * @var array * Array of AssistantSolrQueryString */ protected $_qf = NULL; /** * Query getter */ public function __get($name) { if ( $name == 'q') { if (empty($this->_q)) { $this->_q = new AssistantSolrQueryString(); } return $this->_q; } else if ($name == 'fq') { if (empty($this->_fq)) { $this->_fq = new AssistantSolrQueryString(); } return $this->_fq; } else if ($name == 'qf') { if (empty($this->_qf)) { $this->_qf = new AssistantSolrQueryFilter(); } return $this->_qf; } else if ($name == 'params') { return $this->_params; } else { throw new SolrQueryException("Unsupported parameter " . $name); } } /** * Query setter */ public function __set($name, $value) { if ( $name == 'q') { if (! $value instanceof AssistantSolrQueryString) { throw new SolrQueryException("'q' parameter must be a AssistantSolrQueryString instance"); } $this->_q = $value; } if ( $name == 'fq') { if (! $value instanceof AssistantSolrQueryString) { throw new SolrQueryException("'fq' parameter must be a AssistantSolrQueryString instance"); } $this->_fq = $value; } if ( $name == 'qf') { if (! $value instanceof AssistantSolrQueryFilter) { throw new SolrQueryException("'qf' parameter must be a AssistantSolrQueryFilter instance"); } $this->_qf = $value; } else { throw new SolrQueryException("Unsupported parameter"); } } /** * Rebuild params using setted query strings */ protected function _rebuildParams() { if ($str = (string) $this->_q) { $this->_params->removeParam('q'); $this->_params->addParam('q', $str); } if ($str = (string) $this->_fq) { $this->_params->removeParam('fq'); $this->_params->addParam('fq', $str); } if ($str = (string) $this->_qf) { $this->_params->removeParam('qf'); $this->_params->addParam('qf', $str); } } /** * Get params rebuilt using queries */ public function getParams() { $this->_rebuildParams(); return $this->_params->getParams(); } /** * Construct the object * * @param array $params = array() * Apache Solr module formated array of params * @param AssistantSolrQueryString $q = NULL * A AssistantSolrQueryString instance for Solr Query string construction * @param AssistantSolrQueryString $fq = NULL * A AssistantSolrQueryString instance for Solr FilterQuery string construction */ public function __construct($params = array()) { $this->_params = new AssistantSolrParams($params); } }