Commits

ajaxray committed 9503211

Version b 01 done, stable, tested

Comments (0)

Files changed (8)

+Solr Query Builder
+======================
+
+This project will help to make Solr Queries with an object oriented, dynamic approach. Primarily wrote for using with 
+[solr-php-client](http://code.google.com/p/solr-php-client), but can be used with any solr client that accepts simple string queries.
+
+Features
+--------------------
+
+* Simple queries
+* Automatic escaping
+* Grouping terms
+* Term modifiers
+    * Fuzzy Search
+    * Required/Restricted
+    * Boost factors
+* Range Queries (Date/Numeric)
+* (Any number of) Subqueries
+
+
+Usages Example
+-------------------
+
+Two simple classes made everything here. Field and QueryString. __toString() of both classes just return the raw string query that you can use for searching solr. Subqueries are actually another full featured Query object. So, Any Query object can be used as subquery to another Query.
+
+Here is a small snippet of example - 
+    
+
+    <?php 
+    echo "--------- Solr Query Test ----------- \n";
+    
+    $Query = new Querystring();
+    $Query->addField(new Field('name', 'Anis uddin Ahmad'));
+    $Query->addField(new Field('phone', '+880 173 0053053'), 'PHONE-an-optional-key-for-removing');
+    
+    echo 'Simple query: '. $Query ;
+    // OUTPUT-> Simple query: name:"Anis uddin Ahmad" AND phone:"+880 173 0053053"
+    
+    // Removing fields
+    $Query->removeField('PHONE-an-optional-key-for-removing');
+    
+    echo "--------- Solr SubQuery Test ----------- \n";
+    
+    $subQuery = new  Querystring();
+    $subQuery->addField(new Field(null, 'value for default field'));
+    $subQuery->addField(new Field('item_type', 'anything'));
+    
+    $subQuery->setFieldSeparator('OR');
+    
+    echo 'Subquery Output: '. $subQuery;
+    // OUTPUT-> Subquery Output: "value for default field" OR item_type:anything
+    
+    // Add $subQuery in $Query
+    $Query->addSubQuery($subQuery, 'optional-key');
+    
+    echo 'After adding sub-query: '. $Query;
+    // OUTPUT-> After adding sub-query: name:"Anis uddin Ahmad" AND ("value for default field" OR item_type:anything)
+    ?>
+
+For more examples, check examples/index.php or test files.

classes/field.php

+<?php
+namespace Solr\QueryBuilder;
+
+
+class Field {
+
+    protected $_fieldName;
+    protected $_terms = array();
+
+    const RANGE_QUERY = 'RANGE_QUERY';
+
+    const OPERATOR_RESTRICTED = '-';
+    const OPERATOR_REQUIRED   = '+';
+    
+    private $_operators = array(self::OPERATOR_REQUIRED, self::OPERATOR_RESTRICTED);
+    private $_modifiers = array('~', '^');
+
+
+    function __construct($field = null, $value = null)
+    {
+        if(! empty($field)){
+            $this->_fieldName = strval($field);
+        }
+
+        if(! empty($value)) {
+            $this->addTerm($value);
+        }
+
+        return $this;
+    }
+
+    public function addTerm($value, $operator = null, array $metadata = null)
+    {
+        $term = array();
+
+        $term['value'] = strval($value);
+
+        if(! is_null($operator)) $term['operator'] = $operator;
+        if(! is_null($metadata)) $term['metadata'] = $metadata;
+
+        $this->_terms[] = $term;
+        return $this;
+    }
+
+    public function setRange($from = null, $to = null, $operator = null)
+    {
+        $rangeTerm = array(
+            'from' => (is_null($from))? '*' : strval($from),
+            'to'   => (is_null($to))? '*' : strval($to),
+        );
+
+        $this->addTerm(self::RANGE_QUERY, $operator, $rangeTerm);
+
+        return $this;
+    }
+
+    public function addBoostedTerm($value, $boostFactor, $operator = null)
+    {
+        $boost = array(
+            'modifier' => '^',
+            'factor'   => $boostFactor,
+        );
+
+        if(!is_numeric($boostFactor) || $boostFactor <= 0) {
+            throw new \InvalidArgumentException("Supported boost factor is a number greater than 0, $boostFactor given");
+        } else {
+            $this->addTerm($value, $operator, (($boostFactor == 1)? null : $boost));
+        }
+        
+        return $this;
+    }
+
+    public function addFuzzyTerm($value, $similarity = null, $operator = null)
+    {
+        $fuzzy = array('modifier' => '~');
+
+        if(! is_null($similarity)) {
+            if(!is_numeric($similarity) || $similarity <= 0 || $similarity >= 1) {
+                throw new \InvalidArgumentException("Supported fuzzy similarity factor is a number between 0 to 1, $similarity given");
+            }
+            
+            $fuzzy['factor']   = $similarity;
+        }
+
+        $this->addTerm($value, $operator, $fuzzy);
+
+        return $this;
+    }
+
+    public function addProximityPhrase($value, $distance, $operator = null)
+    {
+        $proximity = array(
+            'modifier' => '~',
+            'factor'   => intval($distance),
+        );
+        $this->addTerm($value, $operator, $proximity);
+
+        return $this;
+    }
+
+    public function __toString()
+    {
+        $output = $this->_processTerms();
+
+        return (empty($this->_fieldName))? $output : $this->_fieldName . ':' . $output;
+    }
+
+    protected function _processTerms()
+    {
+        $termsOutput = (count($this->_terms) > 1)? '(' : '';
+
+        $termSegments = array();
+        foreach($this->_terms as $term){
+
+            if(is_array($term)){
+
+                if($term['value'] == self::RANGE_QUERY){
+                    $segmentOutput = "[{$term['metadata']['from']} TO {$term['metadata']['to']}]";
+                } else {
+                    $segmentOutput = self::_escapeToken($term['value']);
+                }
+
+                // Add operator, required or restricted
+                if(isset($term['operator']) && in_array($term['operator'], $this->_operators)) {
+                    $segmentOutput = $term['operator'] . $segmentOutput;
+                }
+                
+                // Add fuzzy, proximity and boost factor
+                if(isset($term['metadata']['modifier']) && in_array($term['metadata']['modifier'], $this->_modifiers)) {
+                    $segmentOutput .=  $term['metadata']['modifier'];
+                    if(isset($term['metadata']['factor']) && is_numeric($term['metadata']['factor'])){
+                        $segmentOutput .= $term['metadata']['factor'];
+                    }
+                }
+
+                $termSegments[] = $segmentOutput;
+            } else {
+                $termSegments[] = self::_escapeToken($term);
+            }
+        }
+        
+        $termsOutput .= implode(' ', $termSegments);
+        $termsOutput .= (count($this->_terms) > 1)? ')' : '';
+
+        return $termsOutput;
+    }
+
+    /**
+    * Add '"' chars if necessary to a token value.
+    * @return unknown_type
+    */
+    protected static function _escapeToken($token)
+    {
+        if (preg_match("/[\., -:]/", $token)) {
+          return '"' . $token . '"';
+        }
+        else {
+          return $token;
+        }
+    }
+}

classes/param.php

-<?php
-namespace Solr\QueryBuilder;
-
-
-class Param {
-
-    protected $_field;
-    protected $_terms = array();
-
-    private $_operators = array('+', '-');
-    private $_modifiers = array('~', '^');
-
-    const RANGE_QUERY = '#range#';
-
-    function __construct($value = null, $field = null)
-    {
-        if(! empty($value)) {
-            $this->addTerm($value);
-        }
-
-        if(! empty($field)){
-            $this->_field = strval($field);
-        }
-
-        return $this;
-    }
-
-    public function addTerm($value, array $metadata = null)
-    {
-        if($metadata) {
-            $metadata['value'] = strval($value);
-            $this->_terms[] = $metadata;
-        } else {
-            $this->_terms[] = strval($value);
-        }
-
-        return $this;
-    }
-
-    public function addRange($from = null, $to = null)
-    {
-        $rangeTerm = array(
-            'from' => (is_null($from))? '*' : strval($from),
-            'to'   => (is_null($to))? '*' : strval($to),
-        );
-
-        $this->addTerm(self::RANGE_QUERY, $rangeTerm);
-
-        return $this;
-    }
-
-    public function __toString()
-    {
-        $output = $this->_processTerms();
-
-        return (empty($this->_field))? $output : $this->_field . ':' . $output;
-    }
-
-    protected function _processTerms()
-    {
-        $termsOutput = (count($this->_terms) > 1)? '(' : '';
-
-        $termSegments = array();
-        foreach($this->_terms as $term){
-
-            if(is_array($term)){
-
-                if($term['value'] == self::RANGE_QUERY){
-                    $termSegments[] = "[{$term['from']} TO {$term['to']}]";
-                    continue;
-                }
-
-                $segmentOutput = self::_escapeToken($term['value']);
-
-                // Add operator, required or restricted
-                if(isset($term['operator']) && in_array($term['operator'], $this->_operators)) {
-                    $segmentOutput = $term['operator'] . $segmentOutput;
-                }
-                
-                // Add fuzzy, proximity and boost factor
-                if(isset($term['modifier']) && in_array($term['modifier'], $this->_modifiers)) {
-                    $segmentOutput .=  $term['modifier'];
-                    if(isset($term['factor']) && is_numeric($term['factor'])){
-                        $segmentOutput .= $term['factor'];
-                    }
-                }
-
-                $termSegments[] = $segmentOutput;
-            } else {
-                $termSegments[] = self::_escapeToken($term);
-            }
-        }
-        
-        $termsOutput .= implode(' ', $termSegments);
-        $termsOutput .= (count($this->_terms) > 1)? ')' : '';
-
-        return $termsOutput;
-    }
-
-  /**
-   * Add '"' chars if necessary to a token value.
-   * @return unknown_type
-   */
-  protected static function _escapeToken($token) {
-    if (preg_match("/[\., -:]/", $token)) {
-      return '"' . $token . '"';
-    }
-    else {
-      return $token;
-    }
-  }
-}

classes/querystring.php

 namespace Solr\QueryBuilder;
 
 
-class Querystring {
+class Querystring
+{
 
-    const INTERNAL_SEPARATOR = 'in';
-    const EXTERNAL_SEPARATOR = 'ex';
+    /**
+     * Queries storage.
+     *
+     * @var array
+     */
+    protected $_fields = array();
 
-  /**
-   * Queries storage.
-   *
-   * @var array
-   */
-   protected $_queries = array();
+    /**
+     * Sub-queries storage.
+     *
+     * @var array
+     */
+    protected $_subQueries = array();
 
-   /**
-   * Sub-queries storage.
-   *
-   * @var array
-   */
-  protected $_subQueries = array();
+    /**
+     * Default operator for joining fields
+     *
+     * @var string
+     */
+    protected $_fieldSeparator = 'AND';
 
-  /**
-   * Default internal operator. Can be 'AND' or 'OR'/' '.
-   *
-   * @var string
-   */
-  protected $_inOperator = ' ';
+    /**
+     * Add a field to Querystring
+     *
+     * @param Field $field
+     * @param String $key  OPTIONAL identifier to use later for removing or overwriting
+     * @return Querystring
+     */
+    public function addField(Field $field, $key = null)
+    {
+        if ($key) {
+            $this->_fields[$key] = $field;
+        }
+        else {
+            $this->_fields[] = $field;
+        }
 
-  /**
-   * Default external operator. will be used for subqueries Can be 'AND' or 'OR'.
-   *
-   * @var string
-   */
-  protected $_exOperator = 'AND';
+        return $this;
+    }
 
-  /**
-   * Set default operator.
-   *
-   * @param string $operator  Can be 'AND' or 'OR'.
-   * @param string $type      Can be INTERNAL_SEPARATOR or EXTERNAL_SEPARATOR.
-   */
-  public function setOperator($operator, $type = self::INTERNAL_SEPARATOR) {
-    if ($operator == 'AND' || $operator == 'OR') {
-        if($type == self::INTERNAL_SEPARATOR){
-            $this->_inOperator = $operator;
-        } else if($type == self::EXTERNAL_SEPARATOR) {
-            $this->_exOperator = $operator;
+    /**
+     * Add another Querystring object to current query
+     *
+     * @param Querystring $query
+     * @param String $key  OPTIONAL identifier to use later for removing or overwriting
+     * @return Querystring
+     */
+    public function addSubQuery(Querystring $query, $key = null)
+    {
+        if ($key) {
+            $this->_subQueries[$key] = $query;
+        }
+        else {
+            $this->_subQueries[] = $query;
         }
     }
-  }
 
-  /**
-   * Add SubQuery to current query
-   *
-   * @param Querystring $query
-   */
-  public function addSubQuery(Querystring $query, $key = null) {
-      if($key){
-          $this->_subQueries[$key] = $query;
-      } else {
-          $this->_subQueries[] = $query;
-      }
+    /**
+     * Set the operator for joining Fields
+     *
+     * @param  String $separator  AND|OR
+     * @return String Querystring
+     */
+    public function setFieldSeparator($separator)
+    {
+        if ($separator == 'AND' || $separator == 'OR') {
+            $this->_fieldSeparator = $separator;
+        }
 
-  }
-
-  public function removeSubQuery($key) {
-      unset($this->_subQueries[$key]);
-      return $this;
-  }
-
-  public function addQuery(Param $param, $key = null) {
-      if($key){
-          $this->_queries[$key] = $param;
-      } else {
-          $this->_queries[] = $param;
-      }
-
-      return $this;
-  }
-
-  public function removeQuery($key) {
-      unset($this->_queries[$key]);
-      return $this;
-  }
-
-
-  protected function _renderRawOutput() {
-    if (! empty($this->_queries)) {
-      return implode(' '. $this->_inOperator .' ', $this->_queries);
+        return $this;
     }
 
-    return "";
-  }
+    /**
+     * Removes a sub-Query using key
+     *
+     * @param  $key
+     * @return Querystring
+     */
+    public function removeSubQuery($key)
+    {
+        unset($this->_subQueries[$key]);
+        return $this;
+    }
 
-  /**
-   * Build RAW query string
-   *
-   * @return string
-   */
-  public final function __toString() {
-    $output = trim($this->_renderRawOutput());
-    if (! empty($this->_subQueries)) {
-      $subOutput = implode(' ' . $this->_exOperator . ' ', $this->_subQueries);
-      if (! empty($output)) {
-        $output .= ' ' . $this->_exOperator . ' (' . $subOutput . ')';
-      }
-      else {
-        $output .= $subOutput;
-      }
+    /**
+     * Removes a Field using key
+     *
+     * @param  $key
+     * @return Querystring
+     */
+    public function removeField($key)
+    {
+        unset($this->_fields[$key]);
+        return $this;
     }
-    return $output;
-  }
+
+
+    /**
+     * Build RAW query string
+     *
+     * @return string
+     */
+    public final function __toString()
+    {
+        $output = trim($this->_renderRawOutput());
+
+        if (!empty($this->_subQueries)) {
+            $subOutput = ' ('. implode(') ' . $this->_fieldSeparator . ' (', $this->_subQueries) . ') ';
+            if (!empty($output)) {
+                $output .= ' ' . $this->_fieldSeparator . $subOutput;
+            }
+            else {
+                $output = $subOutput;
+            }
+        }
+        return $output;
+    }
+
+    protected function _renderRawOutput()
+    {
+        if (!empty($this->_fields)) {
+            return implode(' ' . $this->_fieldSeparator . ' ', $this->_fields);
+        }
+
+        return "";
+    }
 }
 

examples/index.php

 <?php
-include('querystring.php');
-include('param.php');
+
+include('../classes/querystring.php');
+include('../classes/field.php');
+
+use Solr\QueryBuilder\Querystring;
+use Solr\QueryBuilder\Field;
 
 echo "--------- Solr Query Test ----------- <br />";
 
-$Query = new Solr\QueryBuilder\Querystring();
-$Query->addQuery(new Solr\QueryBuilder\Param('Emran-Ul-Hadi', 'name'));
+$Query = new Querystring();
+$Query->addField(new Field('name', 'Anis uddin Ahmad'));
 echo '<b>simple query:</b> '. $Query ."<br />";
 
-$Query->addQuery(new Solr\QueryBuilder\Param('0179483r50', 'phone'), 'phone');
-$Query->addQuery(new Solr\QueryBuilder\Param('yes', 'active'));
-echo '<b>miltiple fields query:</b> '. $Query ."<br />";
+$Query->addField(new Field('phone', '+880 173 0053053'), 'phone');
+$Query->addField(new Field('active', 'yes'));
+echo '<b>Multiple fields query:</b> '. $Query ."<br />";
 
-$Query->removeQuery('phone');
-$Query->setOperator('AND', $Query::INTERNAL_SEPARATOR);
-echo '<b>after remove phone and internal separator AND:</b> '. $Query ."<br />";
+$Query->removeField('phone');
+$Query->setFieldSeparator('OR');
+echo '<b>After removing phone and separator set to OR:</b> '. $Query ."<br />";
 
-echo "<br /><br />--------- Sub-Solr Query Test ----------- <br />";
-$subQuery = new Solr\QueryBuilder\Querystring();
-$subQuery->addQuery(new Solr\QueryBuilder\Param('for-default-field'));
-$subQuery->addQuery(new Solr\QueryBuilder\Param('diapers', 'item_type'));
+echo "<br /><br />--------- Solr SubQuery Test ----------- <br />";
+$subQuery = new  Querystring();
+$subQuery->addField(new Field(null, 'value for default field'));
+$subQuery->addField(new Field('item_type', 'anything'));
 
 echo '<b>sub query:</b> '. $subQuery ."<br />";
 
-$subQuery->setOperator('AND');
-$Query->addSubQuery($subQuery);
+$Query->addSubQuery($subQuery, 'test-sub-query');
 
 echo '<b>after adding sub-query:</b> '. $Query ."<br />";
 
+
+$subQuery->setFieldSeparator('OR');
+$Query->addSubQuery($subQuery, 'test-sub-query');
+echo '<b>after changing operator to OR between subQuery fields:</b> '. $Query ."<br />";
+
 echo "<br /><br />--------- Term grouping test ----------- <br />";
-$langParam = new Solr\QueryBuilder\Param('en', 'lang');
-$langParam->addTerm('bn-BD')->addTerm('fr');
+$Query->removeSubQuery('test-sub-query');
 
-$Query->addQuery($langParam, 'lang');
+$langParam = new  Field('lang', 'en');
+$langParam->addTerm('bn-BD')
+        ->addTerm('fr');
+
+$Query->addField($langParam, 'lang');
 echo '<b>Added grouped field:</b> '. $Query ."<br />";
 
 echo "<br /><br />--------- Term modifiers test ----------- <br />";
-$category = new Solr\QueryBuilder\Param(null, 'category');
-$category->addTerm('diapers', array('modifier' => '~'));
-$category->addTerm('baby-oil', array('modifier' => '~', 'factor'=> 0.7));
-$Query->addQuery($category, 'category');
+$Query->removeField('lang')
+      ->setFieldSeparator('AND');
+
+$category = new  Field('category');
+$category->addFuzzyTerm('diapers', 0.6);
+$category->addFuzzyTerm('baby-oil', 0.7);
+$Query->addField($category, 'category');
 echo '<b>Fuzzy Search:</b> '. $Query ."<br />";
 
-$category->addTerm('electronics', array('operator' => '-'));
-$category->addTerm('boy-cloths', array('operator' => '+', 'modifier' => '~', 'factor'=> 0.5));
+$category->addTerm('electronics', Field::OPERATOR_RESTRICTED);
+$category->addTerm('boy-cloths', Field::OPERATOR_REQUIRED);
 echo '<b>Required/Restricted:</b> '. $Query ."<br />";
 
-$category->addTerm('baby', array('modifier' => '^', 'factor'=> 3));
-$category->addTerm('food', array('modifier' => '^', 'factor'=> 5));
+$category->addBoostedTerm('baby', 3);
+$category->addBoostedTerm('food', 5);
 echo '<b>Boost:</b> '. $Query ."<br />";
 
 echo "<br /><br />--------- Range Query test ----------- <br />";
-$Query->removeQuery('category');
+$Query->removeField('category');
 
-$date = new Solr\QueryBuilder\Param(null, 'date');
-$date->addRange(null, 'NOW');
+$date = new  Field('date');
+$date->setRange(null, 'NOW');
 
-$age = new Solr\QueryBuilder\Param(null, 'age');
-$age->addRange(18, 60);
+$age = new  Field('age');
+$age->setRange(18, 60);
 
-$Query->addQuery($date, 'date');
-$Query->addQuery($age, 'age');
+$Query->addField($date, 'date');
+$Query->addField($age, 'age');
 echo '<b>Range:</b> '. $Query ."<br />";

tests/FieldTest.php

+<?php
+require('setup.php');
+
+use Solr\QueryBuilder\Field;
+
+/**
+ * Test class for Param.
+ * Generated by PHPUnit on 2011-04-16 at 01:00:29.
+ */
+class FieldTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @var Field
+     */
+    protected $param;
+
+    protected function setUp()
+    {
+        $this->param = new Field('name');
+    }
+
+    public function testSimpleFieldValueTermsWorks()
+    {
+        $this->param->addTerm('toys');
+        $this->assertEquals('name:toys', $this->param->__toString());
+    }
+
+    public function testCanBeInitializedWithASingleValue()
+    {
+        $Param = new Field('name', 'toys');
+        $this->assertEquals('name:toys', $Param->__toString());
+    }
+
+    public function testMultipleTermsAreGroupedTogether()
+    {
+        $this->param->addTerm('toys')
+                ->addTerm('electronics')
+                ->addTerm('food');
+
+        $this->assertEquals('name:(toys electronics food)', $this->param->__toString());
+    }
+
+    public function testSpecialCharactersAreQuoted()
+    {
+        $this->param->addTerm('t[o:y.s')
+                ->addTerm('electr]on-ics')
+                ->addTerm('food');
+
+        $this->assertEquals('name:("t[o:y.s" "electr]on-ics" food)', $this->param->__toString());
+    }
+
+
+    public function testPhrasesAreQuoted()
+    {
+        $this->param->addTerm('baby toys')
+                ->addTerm('electronics')
+                ->addTerm('dry food');
+
+        $this->assertEquals('name:("baby toys" electronics "dry food")', $this->param->__toString());
+    }
+
+    public function testOperatorCanBeSetWithTerm()
+    {
+        $this->param->addTerm('baby toys', Field::OPERATOR_REQUIRED)
+                ->addTerm('electronics', Field::OPERATOR_RESTRICTED)
+                ->addTerm('dry food');
+
+        $this->assertEquals('name:(+"baby toys" -electronics "dry food")', $this->param->__toString());
+    }
+
+    public function testBoostedTermWorks()
+    {
+        $this->param->addBoostedTerm('masjeans', 0.6)
+            ->addBoostedTerm('RBS', 2.4)
+            ->addBoostedTerm('KDS Group', 1);
+
+        $this->assertEquals('name:(masjeans^0.6 RBS^2.4 "KDS Group")', $this->param->__toString());
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     * @dataProvider invalidBoostFactorProvider
+     */
+    public function testBoostedTermThrowsExceptionOnInvalidBoostFactor($boostFactor)
+    {
+        $this->param->addBoostedTerm('WNeeds', $boostFactor);
+        $this->assertEquals("name:WNeeds^$boostFactor", $this->param->__toString());
+    }
+
+    public function testFuzzyTermWorks()
+    {
+        $this->param->addFuzzyTerm('masjeans')
+            ->addFuzzyTerm('RBS', 0.6);
+
+        $this->assertEquals('name:(masjeans~ RBS~0.6)', $this->param->__toString());
+    }
+
+    /**
+     * @expectedException InvalidArgumentException
+     * @dataProvider invalidSimilarityProvider
+     */
+    public function testFuzzyTermThrowsExceptionOnInvalidSimilarity($similarityValue)
+    {
+        $this->param->addFuzzyTerm('WNeeds', $similarityValue);
+        $this->assertEquals("name:WNeeds~$similarityValue", $this->param->__toString());
+    }
+
+    public function testProximityPhraseWorks()
+    {
+        $this->param->addProximityPhrase('Baby Toy', 3);
+        $this->assertEquals('name:"Baby Toy"~3', $this->param->__toString());
+    }
+
+    public function testRangeQueryWorksForDate()
+    {
+        $dateField = new Field('create_date');
+
+        $dateField->setRange('1976-03-06T23:59:59.999Z', 'NOW');
+        $this->assertEquals('create_date:[1976-03-06T23:59:59.999Z TO NOW]', $dateField->__toString());
+    }
+
+    public function testRangeQueryWorksForNumber()
+    {
+        $ageField = new Field('age');
+
+        $ageField->setRange(18, 70);
+        $this->assertEquals('age:[18 TO 70]', $ageField->__toString());
+    }
+
+    public function testRangeQuerySetsStarForOpenEnd()
+    {
+        $ageField = new Field('age');
+        $ageField->setRange(18);
+        $this->assertEquals('age:[18 TO *]', $ageField->__toString());
+
+        $dateField = new Field('create_date');
+        $dateField->setRange(null, 'NOW');
+        $this->assertEquals('create_date:[* TO NOW]', $dateField->__toString());
+    }
+
+
+    //----------- Data provider functions -------------
+
+    public static function invalidSimilarityProvider()
+    {
+        return array(
+            array(-1),
+            array(0),
+            array(1,),
+            array(2,),
+            array('NOT-A-NUMBER')
+        );
+    }
+
+    public static function invalidBoostFactorProvider()
+    {
+        return array(
+            array(-1),
+            array(0),
+            array('NOT-A-NUMBER')
+        );
+    }
+}
+

tests/QuerystringTest.php

+<?php
+require('setup.php');
+
+use Solr\QueryBuilder\Querystring;
+
+/**
+ * Test class for Querystring.
+ * Generated by PHPUnit on 2011-04-16 at 00:59:50.
+ */
+class QuerystringTest extends PHPUnit_Framework_TestCase
+{
+    /**
+     * @var QueryString
+     */
+    private $_queryString;
+
+    /**
+     * @var PHPUnit_Framework_MockObject_MockObject
+     */
+    private $_field;
+
+    protected function setUp()
+    {
+        $this->_queryString = new Querystring;
+
+        $this->_field = $this->getMock('\Solr\QueryBuilder\Field', array('__toString'));
+        $this->_field->expects($this->any())
+            ->method('__toString')
+            ->will($this->returnValue('FIELD:VALUE'));
+    }
+
+    /**
+     * Test summary
+     * ------------------
+     * - Simple single field query
+     * - Multiple field query
+     * - Operator between fields
+     * - Removing Field
+     *
+     * - Adding a SubQuery
+     * - Adding multiple SubQueries
+     * - Operator for SubQuery
+     * - Removing SubQuery
+     */
+
+    public function testAddfieldWorksForSingleField()
+    {
+        $this->_queryString->addField($this->_field);
+        $this->assertEquals('FIELD:VALUE', $this->_queryString->__toString());
+    }
+
+    public function testAddfieldWorksForMultipleFields()
+    {
+        $this->_queryString->addField($this->_field);
+        $this->_queryString->addField(clone $this->_field);
+        $this->_queryString->addField(clone $this->_field);
+
+        $this->assertEquals('FIELD:VALUE AND FIELD:VALUE AND FIELD:VALUE', $this->_queryString->__toString());
+    }
+    
+    /**
+     * @covers Querystring::setOperator
+     */
+    public function testSetFieldSeparatorChangesOperatorBetweenFields()
+    {
+        $this->_queryString->addField($this->_field);
+        $this->_queryString->addField(clone $this->_field);
+        $this->_queryString->addField(clone $this->_field);
+
+        // OR is default separator
+        $this->_queryString->setFieldSeparator('AND');
+        $this->assertEquals('FIELD:VALUE AND FIELD:VALUE AND FIELD:VALUE', $this->_queryString->__toString());
+
+        $this->_queryString->setFieldSeparator('OR');
+        $this->assertEquals('FIELD:VALUE OR FIELD:VALUE OR FIELD:VALUE', $this->_queryString->__toString());
+    }
+
+    public function testRemoveFieldRemovesAFieldByKey()
+    {
+        $newField = $this->getMock('\Solr\QueryBuilder\Field', array('__toString'));
+        $newField->expects($this->any())
+                ->method('__toString')
+                ->will($this->returnValue('NEWFIELD:NEWVALUE'));
+
+        $this->_queryString->addField($this->_field);
+        $this->_queryString->addField($newField, 'new-field');
+        $this->assertEquals('FIELD:VALUE AND NEWFIELD:NEWVALUE', $this->_queryString->__toString());
+
+        $this->_queryString->removeField('new-field');
+        $this->assertEquals('FIELD:VALUE', $this->_queryString->__toString());
+    }
+
+    public function testAddSubqueryWorksForSingleQuery()
+    {
+        $subQuery = new Querystring();
+        $subQuery->addField(clone $this->_field);
+
+        $this->_queryString->addField($this->_field);
+        $this->_queryString->addSubQuery($subQuery);
+
+        $this->assertEquals('FIELD:VALUE AND (FIELD:VALUE) ', $this->_queryString->__toString());
+    }
+
+    public function testAddSubqueryWorksForMultipleQueries()
+    {
+        $subQuery1 = new Querystring();
+        $subQuery1->addField(clone $this->_field);
+
+        $subQuery2 = new Querystring();
+        $subQuery2->addField(clone $this->_field)
+                ->addField(clone $this->_field);
+
+        $this->_queryString->addField(clone $this->_field);
+        $this->_queryString->addSubQuery($subQuery1);
+        $this->_queryString->addSubQuery($subQuery2);
+        $this->_queryString->addField(clone $this->_field);
+
+        $this->assertEquals('FIELD:VALUE AND FIELD:VALUE AND (FIELD:VALUE) AND (FIELD:VALUE AND FIELD:VALUE) ', $this->_queryString->__toString());
+    }
+
+    /**
+     * @covers Querystring::setOperator
+     */
+    public function testSetFieldSeparatorChangesOperatorInSubQueries()
+    {
+        $subQuery1 = new Querystring();
+        $subQuery1->addField(clone $this->_field)
+                ->addField(clone $this->_field);
+
+        $subQuery2 = new Querystring();
+        $subQuery2->addField(clone $this->_field)
+                ->addField(clone $this->_field)
+                ->setFieldSeparator('OR');
+
+        $this->_queryString->addField(clone $this->_field);
+        $this->_queryString->addSubQuery($subQuery1);
+        $this->_queryString->addSubQuery($subQuery2);
+        $this->_queryString->addField(clone $this->_field);
+
+        $this->assertEquals('FIELD:VALUE AND FIELD:VALUE AND (FIELD:VALUE AND FIELD:VALUE) AND (FIELD:VALUE OR FIELD:VALUE) ', $this->_queryString->__toString());
+
+        return $this->_queryString;
+    }
+
+    /**
+     * @param Querystring $queryString
+     *
+     * @depends testSetFieldSeparatorChangesOperatorInSubQueries
+     * @return void
+     */
+    public function testRemoveSubqueryRemovesAQueryByKey(Querystring $queryString)
+    {
+        // Test with auto numeric key
+        $queryString->removeSubQuery(1);
+        $this->assertEquals('FIELD:VALUE AND FIELD:VALUE AND (FIELD:VALUE AND FIELD:VALUE) ', $queryString->__toString());
+
+        $subQuery3 = new Querystring();
+        $subQuery3->addField(clone $this->_field);
+
+        // Test with string key
+        $queryString->addSubQuery($subQuery3, 'third-sub-query');
+        $this->assertEquals('FIELD:VALUE AND FIELD:VALUE AND (FIELD:VALUE AND FIELD:VALUE) AND (FIELD:VALUE) ', $queryString->__toString());
+        
+        $queryString->removeSubQuery('third-sub-query');
+        $this->assertEquals('FIELD:VALUE AND FIELD:VALUE AND (FIELD:VALUE AND FIELD:VALUE) ', $queryString->__toString());
+    }
+}
+<?php
+/**
+ * === Setup the Env and Includes fo testing ======
+ *
+ * Author: Anis uddin Ahmad <anisniit@gmail.com>
+ * Created On: 7/10/11 1:18 AM
+ */
+
+require_once 'PHPUnit/Framework.php';
+
+include('../classes/querystring.php');
+include('../classes/field.php');