├── runtests.sh ├── .gitignore ├── tests ├── Base.php ├── install-php-memcache.sh ├── Helper.php └── units │ ├── Stringify.php │ ├── Mapping.php │ ├── Builder.php │ └── Client.php ├── .travis.yml ├── src └── ElasticSearch │ ├── Exception.php │ ├── Transport │ ├── HTTPException.php │ ├── Base.php │ ├── Memcached.php │ └── HTTP.php │ ├── Mapping.php │ ├── DSL │ ├── Builder.php │ ├── Query.php │ ├── RangeQuery.php │ └── Stringify.php │ ├── Bulk.php │ └── Client.php ├── composer.json ├── LICENSE ├── README.markdown └── docs └── dsl_spec /runtests.sh: -------------------------------------------------------------------------------- 1 | ./vendor/bin/atoum --ulr -d tests/units 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | vendor 3 | .DS_Store 4 | composer.phar 5 | composer.lock 6 | .vagrant/ 7 | Vagrantfile 8 | -------------------------------------------------------------------------------- /tests/Base.php: -------------------------------------------------------------------------------- 1 | > `php --ini | grep "Loaded Configuration" | sed -e "s|.*:\s*||"` 7 | exit 8 | -------------------------------------------------------------------------------- /src/ElasticSearch/Exception.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | class Exception extends \Exception { 14 | } 15 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "nervetattoo/elasticsearch", 3 | "type" : "library", 4 | "description" : "ElasticSearch client for PHP 5.3", 5 | "keywords" : ["elasticsearch", "client"], 6 | "homepage" : "http://github.com/nervetattoo/elasticsearch", 7 | "license" : "MIT", 8 | "authors" : [ 9 | { 10 | "name" : "Raymond Julin", 11 | "email" : "raymond.julin@gmail.com", 12 | "homepage" : "http://raymondjulin.com" 13 | } 14 | ], 15 | "require" : { 16 | "php" : ">=5.3.0" 17 | }, 18 | "require-dev" : { 19 | "atoum/atoum" : "^2.9" 20 | }, 21 | "autoload" : { 22 | "psr-0" : { 23 | "ElasticSearch" : "src/" 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Helper.php: -------------------------------------------------------------------------------- 1 | 0) { 10 | shuffle($words); 11 | $sentence .= $words[0] . " "; 12 | $len--; 13 | } 14 | return array('title' => $sentence, 'rank' => rand(1, 10)); 15 | } 16 | 17 | public static function addDocuments(\ElasticSearch\Client $client, $num = 3, $tag = 'cool') 18 | { 19 | $options = array('refresh' => true); 20 | while ($num-- > 0) { 21 | $doc = array('title' => "One cool document $tag", 'rank' => rand(1,10)); 22 | $client->index($doc, $num + 1, $options); 23 | } 24 | return $client; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010-2012 Raymond Julin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /src/ElasticSearch/Transport/HTTPException.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | class HTTPException extends \Exception { 15 | /** 16 | * Exception data 17 | * @var array 18 | */ 19 | protected $data = array( 20 | 'payload' => null, 21 | 'protocol' => null, 22 | 'port' => null, 23 | 'host' => null, 24 | 'url' => null, 25 | 'method' => null, 26 | ); 27 | 28 | /** 29 | * Setter 30 | * @param mixed $key 31 | * @param mixed $value 32 | */ 33 | public function __set($key, $value) { 34 | if (array_key_exists($key, $this->data)) 35 | $this->data[$key] = $value; 36 | } 37 | 38 | /** 39 | * Getter 40 | * @param mixed $key 41 | * @return mixed 42 | */ 43 | public function __get($key) { 44 | if (array_key_exists($key, $this->data)) 45 | return $this->data[$key]; 46 | else 47 | return false; 48 | } 49 | 50 | /** 51 | * Rebuild CLI command using curl to further investigate the failure 52 | * @return string 53 | */ 54 | public function getCLICommand() { 55 | $postData = json_encode($this->payload); 56 | $curlCall = "curl -X{$method} 'http://{$this->host}:{$this->port}$this->url' -d '$postData'"; 57 | return $curlCall; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/ElasticSearch/Mapping.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | class Mapping { 15 | 16 | protected $properties = array(); 17 | protected $config = array(); 18 | 19 | /** 20 | * Build mapping data 21 | * 22 | * @param array $properties 23 | * @param array $config 24 | * @return \ElasticSearch\Mapping 25 | */ 26 | public function __construct(array $properties = array(), array $config = array()) { 27 | $this->properties = $properties; 28 | $this->config = $config; 29 | } 30 | 31 | /** 32 | * Export mapping data as a json-ready array 33 | * 34 | * @return string 35 | */ 36 | public function export() { 37 | return array( 38 | 'properties' => $this->properties 39 | ); 40 | } 41 | 42 | /** 43 | * Add or overwrite existing field by name 44 | * 45 | * @param string $field 46 | * @param string|array $config 47 | * @return $this 48 | */ 49 | public function field($field, $config = array()) { 50 | if (is_string($config)) $config = array('type' => $config); 51 | $this->properties[$field] = $config; 52 | return $this; 53 | } 54 | 55 | /** 56 | * Get or set a config 57 | * 58 | * @param string $key 59 | * @param mixed $value 60 | * @throws \Exception 61 | * @return array|void 62 | */ 63 | public function config($key, $value = null) { 64 | if (is_array($key)) 65 | $this->config = $key + $this->config; 66 | else { 67 | if ($value !== null) $this->config[$key] = $value; 68 | if (!isset($this->config[$key])) 69 | throw new \Exception("Configuration key `type` is not set"); 70 | return $this->config[$key]; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/ElasticSearch/DSL/Builder.php: -------------------------------------------------------------------------------- 1 | 8 | * 9 | * For the full copyright and license information, please view the LICENSE 10 | * file that was distributed with this source code. 11 | */ 12 | 13 | /** 14 | * Helper stuff for working with the ElasticSearch DSL 15 | * How to build a mildly complex query: 16 | * $dsl = new ElasticSearchDSL; 17 | * $bool = $dsl->bool(); // Return a new bool structure 18 | * 19 | * @author Raymond Julin 20 | * @package ElasticSearchClient 21 | * @since 0.1 22 | * Created: 2010-07-23 23 | */ 24 | class Builder { 25 | 26 | protected $dsl = array(); 27 | 28 | private $explain = null; 29 | private $from = null; 30 | private $size = null; 31 | private $fields = null; 32 | private $query = null; 33 | private $facets = null; 34 | private $sort = null; 35 | 36 | /** 37 | * Construct DSL object 38 | * 39 | * @return \ElasticSearch\DSL\Builder 40 | * @param array $options 41 | */ 42 | public function __construct(array $options=array()) { 43 | foreach ($options as $key => $value) 44 | $this->$key = $value; 45 | } 46 | 47 | /** 48 | * Add array clause, can only be one 49 | * 50 | * @return \ElasticSearch\DSL\Query 51 | * @param array $options 52 | */ 53 | public function query(array $options=array()) { 54 | if (!($this->query instanceof Query)) 55 | $this->query = new Query($options); 56 | return $this->query; 57 | } 58 | 59 | /** 60 | * Build the DSL as array 61 | * 62 | * @throws \ElasticSearch\Exception 63 | * @return array 64 | */ 65 | public function build() { 66 | $built = array(); 67 | if ($this->from != null) 68 | $built['from'] = $this->from; 69 | if ($this->size != null) 70 | $built['size'] = $this->size; 71 | if ($this->sort && is_array($this->sort)) 72 | $built['sort'] = $this->sort; 73 | if (!$this->query) 74 | throw new \ElasticSearch\Exception("Query must be specified"); 75 | else 76 | $built['query'] = $this->query->build(); 77 | return $built; 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/ElasticSearch/DSL/Query.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | /** 15 | * Handle the query sub dsl 16 | * 17 | * @author Raymond Julin 18 | * @package ElasticSearchClient 19 | * @since 0.1 20 | * Created: 2010-07-24 21 | */ 22 | class Query { 23 | protected $term = null; 24 | /** 25 | * @var RangeQuery 26 | */ 27 | protected $range; 28 | protected $prefix = null; 29 | protected $wildcard = null; 30 | protected $matchAll = null; 31 | protected $queryString = null; 32 | protected $bool = null; 33 | protected $disMax = null; 34 | protected $constantScore = null; 35 | protected $filteredQuery = null; 36 | 37 | public function __construct(array $options=array()) { 38 | } 39 | 40 | /** 41 | * Add a term to this query 42 | * 43 | * @return \ElasticSearch\DSL\Query 44 | * @param string $term 45 | * @param bool|string $field 46 | */ 47 | public function term($term, $field=false) { 48 | $this->term = ($field) 49 | ? array($field => $term) 50 | : $term; 51 | return $this; 52 | } 53 | 54 | /** 55 | * Add a wildcard to this query 56 | * 57 | * @return \ElasticSearch\DSL\Query 58 | * @param $val 59 | * @param bool|string $field 60 | */ 61 | public function wildcard($val, $field=false) { 62 | $this->wildcard = ($field) 63 | ? array($field => $val) 64 | : $val; 65 | return $this; 66 | } 67 | 68 | /** 69 | * Add a range query 70 | * 71 | * @return \ElasticSearch\DSL\RangeQuery 72 | * @param array $options 73 | */ 74 | public function range(array $options=array()) { 75 | $this->range = new RangeQuery($options); 76 | return $this->range; 77 | } 78 | 79 | /** 80 | * Build the DSL as array 81 | * 82 | * @return array 83 | */ 84 | public function build() { 85 | $built = array(); 86 | if ($this->term) 87 | $built['term'] = $this->term; 88 | elseif ($this->range) 89 | $built['range'] = $this->range->build(); 90 | elseif ($this->wildcard) 91 | $built['wildcard'] = $this->wildcard; 92 | return $built; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /tests/units/Stringify.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * For the full copyright and license information, please view the LICENSE 12 | * file that was distributed with this source code. 13 | */ 14 | class Stringify extends \ElasticSearch\tests\Base 15 | { 16 | public function testNamedTerm() { 17 | $arr = array( 18 | 'query' => array( 19 | 'term' => array('title' => 'cool') 20 | ) 21 | ); 22 | $dsl = new \ElasticSearch\DSL\Stringify($arr); 23 | $strDsl = (string)$dsl; 24 | $this->assert->string((string) $dsl) 25 | ->isEqualTo('title:cool'); 26 | } 27 | 28 | public function testTerm() { 29 | $arr = array( 30 | 'query' => array( 31 | 'term' => 'cool' 32 | ) 33 | ); 34 | $dsl = new \ElasticSearch\DSL\Stringify($arr); 35 | $this->assert->string((string) $dsl) 36 | ->isEqualTo('cool'); 37 | } 38 | 39 | public function testGroupedTerms() { 40 | $arr = array( 41 | 'query' => array( 42 | 'term' => 'cool stuff' 43 | ) 44 | ); 45 | $dsl = new \ElasticSearch\DSL\Stringify($arr); 46 | $this->assert->string((string) $dsl) 47 | ->isEqualTo('"cool stuff"'); 48 | } 49 | 50 | public function testNamedGroupedTerms() { 51 | $arr = array( 52 | 'query' => array( 53 | 'term' => array('title' => 'cool stuff') 54 | ) 55 | ); 56 | $dsl = new \ElasticSearch\DSL\Stringify($arr); 57 | $this->assert->string((string) $dsl) 58 | ->isEqualTo('title:"cool stuff"'); 59 | } 60 | 61 | public function testSort() { 62 | $arr = array( 63 | 'sort' => array( 64 | array('title' => 'desc') 65 | ), 66 | 'query' => array( 67 | 'term' => array('title' => 'cool stuff') 68 | ) 69 | ); 70 | $dsl = new \ElasticSearch\DSL\Stringify($arr); 71 | $this->assert->string((string) $dsl) 72 | ->isEqualTo('title:"cool stuff"&sort=title:reverse'); 73 | 74 | $arr['sort'] = array('title'); 75 | $dsl = new \ElasticSearch\DSL\Stringify($arr); 76 | $this->assert->string((string) $dsl) 77 | ->isEqualTo('title:"cool stuff"&sort=title'); 78 | 79 | $arr['sort'] = array(array('title' => array('reverse' => true))); 80 | $dsl = new \ElasticSearch\DSL\Stringify($arr); 81 | $this->assert->string((string) $dsl) 82 | ->isEqualTo('title:"cool stuff"&sort=title:reverse'); 83 | } 84 | 85 | public function testLimitReturnFields() { 86 | $arr = array( 87 | 'fields' => array('title','body'), 88 | 'query' => array( 89 | 'term' => array('title' => 'cool') 90 | ) 91 | ); 92 | $dsl = new \ElasticSearch\DSL\Stringify($arr); 93 | $this->assert->string((string) $dsl) 94 | ->isEqualTo('title:cool&fields=title,body'); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/ElasticSearch/DSL/RangeQuery.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | /** 15 | * Range queries 16 | * 17 | * @author Raymond Julin 18 | * @package ElasticSearchClient 19 | * @since 0.1 20 | * Created: 2010-07-24 21 | */ 22 | class RangeQuery { 23 | protected $fieldname = null; 24 | protected $from = null; 25 | protected $to = null; 26 | protected $includeLower = null; 27 | protected $includeUpper = null; 28 | protected $boost = null; 29 | 30 | 31 | /** 32 | * Construct new RangeQuery component 33 | * 34 | * @return \ElasticSearch\DSL\RangeQuery 35 | * @param array $options 36 | */ 37 | public function __construct(array $options=array()) { 38 | $this->fieldname = key($options); 39 | $values = current($options); 40 | if (is_array($values)) { 41 | foreach ($values as $key => $val) 42 | $this->$key = $val; 43 | } 44 | } 45 | 46 | /** 47 | * Setters 48 | * 49 | * @return \ElasticSearch\DSL\RangeQuery 50 | * @param mixed $value 51 | */ 52 | public function fieldname($value) { 53 | $this->fieldname = $value; 54 | return $this; 55 | } 56 | 57 | /** 58 | * @param $value 59 | * @return \ElasticSearch\DSL\RangeQuery $this 60 | */ 61 | public function from($value) { 62 | $this->from = $value; 63 | return $this; 64 | } 65 | /** 66 | * @param $value 67 | * @return \ElasticSearch\DSL\RangeQuery $this 68 | */ 69 | public function to($value) { 70 | $this->to = $value; 71 | return $this; 72 | } 73 | /** 74 | * @param $value 75 | * @return \ElasticSearch\DSL\RangeQuery $this 76 | */ 77 | public function includeLower($value) { 78 | $this->includeLower = $value; 79 | return $this; 80 | } 81 | /** 82 | * @param $value 83 | * @return \ElasticSearch\DSL\RangeQuery $this 84 | */ 85 | public function includeUpper($value) { 86 | $this->includeUpper = $value; 87 | return $this; 88 | } 89 | /** 90 | * @param $value 91 | * @return \ElasticSearch\DSL\RangeQuery $this 92 | */ 93 | public function boost($value) { 94 | $this->boost = $value; 95 | return $this; 96 | } 97 | 98 | /** 99 | * Build to array 100 | * 101 | * @throws \ElasticSearch\Exception 102 | * @return array 103 | */ 104 | public function build() { 105 | $built = array(); 106 | if ($this->fieldname) { 107 | $built[$this->fieldname] = array(); 108 | foreach (array("from","to","includeLower","includeUpper", "boost") as $opt) { 109 | if ($this->$opt !== null) 110 | $built[$this->fieldname][$opt] = $this->$opt; 111 | } 112 | if (count($built[$this->fieldname]) == 0) 113 | throw new \ElasticSearch\Exception("Empty RangeQuery cant be created"); 114 | } 115 | return $built; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /tests/units/Mapping.php: -------------------------------------------------------------------------------- 1 | setIndex('test-index')->delete(); 13 | \ElasticSearch\Client::connection()->delete(); 14 | } 15 | 16 | public function testMapCreate() { 17 | $mapping = new \ElasticSearch\Mapping(array( 18 | 'tweet' => array( 19 | 'type' => 'string' 20 | ), 21 | 'user.name' => array( 22 | 'index' => 'not_analyzed' 23 | ) 24 | )); 25 | 26 | $jsonBody = $mapping->export(); 27 | 28 | $this->assert->array($jsonBody) 29 | ->isNotEmpty() 30 | ->hasSize(1) 31 | ->array($jsonBody['properties']) 32 | ->hasSize(2); 33 | 34 | $properties = $jsonBody['properties']; 35 | 36 | $this->assert->array($properties['tweet']) 37 | ->isNotEmpty() 38 | ->isEqualTo(array( 39 | 'type' => 'string' 40 | )); 41 | 42 | $this->assert->array($properties['user.name']) 43 | ->isNotEmpty() 44 | ->isEqualTo(array( 45 | 'index' => 'not_analyzed' 46 | )); 47 | } 48 | 49 | public function testAddMoreFieldsToMapping() { 50 | $mapping = new \ElasticSearch\Mapping; 51 | 52 | $exported = $mapping->field('tweet', array( 53 | 'type' => 'string' 54 | ))->export(); 55 | 56 | // TODO Does atoum have a prettier interface for this drill down? 57 | $this->assert->array($exported)->isNotEmpty() 58 | ->isEqualTo(array( 59 | 'properties' => array( 60 | 'tweet' => array('type' => 'string') 61 | ) 62 | )); 63 | } 64 | 65 | public function testAddFieldWithTypeLazy() { 66 | $mapping = new \ElasticSearch\Mapping; 67 | // Basic mappings: 68 | $exported = $mapping->field('tweet', 'string')->export(); 69 | $this->assert->array($exported)->isNotEmpty() 70 | ->isEqualTo(array( 71 | 'properties' => array( 72 | 'tweet' => array('type' => 'string') 73 | ) 74 | )); 75 | } 76 | 77 | public function testAddTypeConstrainedMapping() { 78 | $mapping = new \ElasticSearch\Mapping(array( 79 | 'tweet' => array( 80 | 'type' => 'string' 81 | ) 82 | ), array('type' => 'tweet')); 83 | 84 | $exported = $mapping->export(); 85 | $this->assert->array($exported)->isNotEmpty() 86 | ->hasKey('properties')->notHasKey('type'); 87 | } 88 | 89 | // Integrate and perform query 90 | public function testMapFields() { 91 | $client = \ElasticSearch\Client::connection(array( 92 | 'index' => 'test-index', 93 | 'type' => 'test-type' 94 | )); 95 | $client->index(array( 96 | 'tweet' => 'ElasticSearch is awesome' 97 | )); 98 | $response = $client->map(array( 99 | 'tweet' => array('type' => 'string') 100 | )); 101 | 102 | $this->assert->array($response)->isNotEmpty(); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/units/Builder.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * For the full copyright and license information, please view the LICENSE 12 | * file that was distributed with this source code. 13 | */ 14 | 15 | class Builder extends \ElasticSearch\tests\Base 16 | { 17 | public function testTermQuery() { 18 | $dsl = new \ElasticSearch\DSL\Builder; 19 | $query = $dsl->query(); 20 | $query->term("cool", "title"); 21 | 22 | $arr = array( 23 | 'query' => array( 24 | 'term' => array('title' => 'cool') 25 | ) 26 | ); 27 | $built = $dsl->build(); 28 | $this->assert->array($built)->isEqualTo($arr); 29 | 30 | $query->wildcard("cool*", "title"); 31 | $this->assert->array($dsl->build())->isEqualTo($arr); 32 | } 33 | 34 | public function testRangeQuery() { 35 | $dsl = new \ElasticSearch\DSL\Builder; 36 | $query = $dsl->query(); 37 | $query->range(array( 38 | 'age' => array( 39 | 'from' => 18, 40 | 'to' => 100, 41 | 'includeLower' => true, 42 | 'includeUpper' => false, 43 | 'boost' => 2.0 44 | ) 45 | )); 46 | 47 | // This is how it should turn out 48 | $arr = array( 49 | 'query' => array( 50 | 'range' => array( 51 | 'age' => array( 52 | 'from' => 18, 53 | 'to' => 100, 54 | 'includeLower' => true, 55 | 'includeUpper' => false, 56 | 'boost' => 2.0 57 | ) 58 | ) 59 | ) 60 | ); 61 | 62 | $this->assert->array($dsl->build())->isEqualTo($arr); 63 | } 64 | 65 | public function testRangeQueryAlternativeSyntax() { 66 | $dsl = new \ElasticSearch\DSL\Builder; 67 | $query = $dsl->query(); 68 | $range = $query->range(); 69 | $range->fieldname('age') 70 | ->from(18) 71 | ->to(100) 72 | ->includeUpper(false) 73 | ->includeLower(false) 74 | ->boost(2.0); 75 | 76 | // This is how it should turn out 77 | $arr = array( 78 | 'query' => array( 79 | 'range' => array( 80 | 'age' => array( 81 | 'from' => 18, 82 | 'to' => 100, 83 | 'includeLower' => false, 84 | 'includeUpper' => false, 85 | 'boost' => 2.0 86 | ) 87 | ) 88 | ) 89 | ); 90 | 91 | $this->assert->array($dsl->build())->isEqualTo($arr); 92 | } 93 | 94 | public function testSortClause() { 95 | $sort = array('title' => 'desc'); 96 | $dsl = new \ElasticSearch\DSL\Builder(compact('sort')); 97 | $dsl->query()->term("cool", "title"); 98 | 99 | $this->assert->array($dsl->build()) 100 | ->isEqualTo(array( 101 | 'query' => array( 102 | 'term' => array('title' => 'cool') 103 | ), 104 | 'sort' => $sort 105 | )); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/ElasticSearch/DSL/Stringify.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | /** 15 | * Parse a DSL object into a string based representation 16 | * Return string representation of DSL for search. 17 | * This will remove certain fields that are not supported 18 | * in a string representation 19 | * 20 | * @author Raymond Julin 21 | * @package ElasticSearch 22 | * @since 0.1 23 | * Created: 2010-07-24 24 | */ 25 | class Stringify { 26 | 27 | protected $dsl = array(); 28 | 29 | public function __construct(array $dsl) { 30 | $this->dsl = $dsl; 31 | } 32 | 33 | public function __toString() { 34 | $dsl = $this->dsl; 35 | $query = $dsl['query']; 36 | 37 | $string = ""; 38 | if (array_key_exists("term", $query)) 39 | $string .= $this->transformDSLTermToString($query['term']); 40 | if (array_key_exists("wildcard", $query)) 41 | $string .= $this->transformDSLTermToString($query['wildcard']); 42 | if (array_key_exists("sort", $dsl)) 43 | $string .= $this->transformDSLSortToString($dsl['sort']); 44 | if (array_key_exists("fields", $dsl)) 45 | $string .= $this->transformDSLFieldsToString($dsl['fields']); 46 | return $string; 47 | } 48 | 49 | /** 50 | * A naive transformation of possible term and wildcard arrays in a DSL 51 | * query 52 | * 53 | * @return string 54 | * @param mixed $dslTerm 55 | */ 56 | protected function transformDSLTermToString($dslTerm) { 57 | $string = ""; 58 | if (is_array($dslTerm)) { 59 | $key = key($dslTerm); 60 | $value = $dslTerm[$key]; 61 | if (is_string($key)) 62 | $string .= "$key:"; 63 | } 64 | else 65 | $value = $dslTerm; 66 | /** 67 | * If a specific key is used as key in the array 68 | * this should translate to searching in a specific field (field:term) 69 | */ 70 | if (strpos($value, " ") !== false) 71 | $string .= '"' . $value . '"'; 72 | else 73 | $string .= $value; 74 | return $string; 75 | } 76 | 77 | /** 78 | * Transform search parameters to string 79 | * 80 | * @return string 81 | * @param mixed $dslSort 82 | */ 83 | protected function transformDSLSortToString($dslSort) { 84 | $string = ""; 85 | if (is_array($dslSort)) { 86 | foreach ($dslSort as $sort) { 87 | if (is_array($sort)) { 88 | $field = key($sort); 89 | $info = current($sort); 90 | } 91 | else 92 | $field = $sort; 93 | $string .= "&sort=" . $field; 94 | if (isset($info)) { 95 | if (is_string($info) && $info == "desc") 96 | $string .= ":reverse"; 97 | elseif (is_array($info) && array_key_exists("reverse", $info) && $info['reverse']) 98 | $string .= ":reverse"; 99 | } 100 | } 101 | } 102 | return $string; 103 | } 104 | 105 | /** 106 | * Transform a selection of fields to return to string form 107 | * 108 | * @return string 109 | * @param mixed $dslFields 110 | */ 111 | protected function transformDSLFieldsToString($dslFields) { 112 | $string = ""; 113 | if (is_array($dslFields)) 114 | $string .= "&fields=" . join(",", $dslFields); 115 | return $string; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/ElasticSearch/Bulk.php: -------------------------------------------------------------------------------- 1 | client = $client; 18 | } 19 | 20 | /** 21 | * commit this operation 22 | */ 23 | public function commit() { 24 | return $this->client->request('/_bulk', 'POST', $this->createPayload()); 25 | } 26 | 27 | /** 28 | * reset this operation 29 | */ 30 | public function reset() { 31 | $this->operations = array(); 32 | } 33 | 34 | /** 35 | * Index a new document or update it if existing 36 | * 37 | * @param array $document 38 | * @param mixed $id Optional 39 | * @param string $index Index 40 | * @param string $type Type 41 | * @param array $options Allow sending query parameters to control indexing further 42 | * _refresh_ *bool* If set to true, immediately refresh the shard after indexing 43 | * @return \Elasticsearch\Bulk 44 | */ 45 | public function index($document, $id=null, $index, $type, array $options = array()) { 46 | $params = array( '_id' => $id, 47 | '_index' => $index, 48 | '_type' => $type); 49 | 50 | foreach ($options as $key => $value) { 51 | $params['_' . $key] = $value; 52 | } 53 | 54 | $operation = array( 55 | array('index' => $params), 56 | $document 57 | ); 58 | $this->operations[] = $operation; 59 | return $this; 60 | } 61 | 62 | /** 63 | * Update a part of a document 64 | * 65 | * @param array $partialDocument 66 | * @param mixed $id 67 | * @param string $index Index 68 | * @param string $type Type 69 | * @param array $options Allow sending query parameters to control indexing further 70 | * _refresh_ *bool* If set to true, immediately refresh the shard after indexing 71 | * 72 | * @return \Elasticsearch\Bulk 73 | */ 74 | public function update($partialDocument, $id, $index, $type, array $options = array()) { 75 | $params = array( 76 | '_id' => $id, 77 | '_index' => $index, 78 | '_type' => $type, 79 | ); 80 | 81 | foreach ($options as $key => $value) { 82 | $params['_'.$key] = $value; 83 | } 84 | 85 | $operation = array( 86 | array('update' => $params), 87 | array('doc' => $partialDocument), 88 | ); 89 | $this->operations[] = $operation; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * delete a document 96 | * 97 | * @param mixed $id 98 | * @param string $index Index 99 | * @param string $type Type 100 | * @param array $options Parameters to pass to delete action 101 | * @return \Elasticsearch\Bulk 102 | */ 103 | public function delete($id=false, $index, $type, array $options = array()) { 104 | $params = array( '_id' => $id, 105 | '_index' => $index, 106 | '_type' => $type); 107 | 108 | foreach ($options as $key => $value) { 109 | $params['_' . $key] = $value; 110 | } 111 | 112 | $operation = array( 113 | array('delete' => $params) 114 | ); 115 | $this->operations[] = $operation; 116 | return $this; 117 | 118 | } 119 | 120 | /** 121 | * get all pending operations 122 | * @return array 123 | */ 124 | public function getOperations() { 125 | return $this->operations; 126 | } 127 | 128 | /** 129 | * count all pending operations 130 | * @return int 131 | */ 132 | public function count() { 133 | return count($this->operations); 134 | } 135 | 136 | /** 137 | * create a request payload with all pending operations 138 | * @return string 139 | */ 140 | public function createPayload() 141 | { 142 | $payloads = array(); 143 | foreach ($this->operations as $operation) { 144 | foreach ($operation as $partial) { 145 | $payloads[] = json_encode($partial); 146 | } 147 | } 148 | return join("\n", $payloads)."\n"; 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/ElasticSearch/Transport/Base.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | abstract class Base { 15 | 16 | /** 17 | * What host to connect to for server 18 | * @var string 19 | */ 20 | protected $host = ""; 21 | 22 | /** 23 | * Port to connect on 24 | * @var int 25 | */ 26 | protected $port = 9200; 27 | 28 | /** 29 | * ElasticSearch index 30 | * @var string 31 | */ 32 | protected $index; 33 | 34 | /** 35 | * ElasticSearch document type 36 | * @var string 37 | */ 38 | protected $type; 39 | 40 | /** 41 | * Default constructor, just set host and port 42 | * @param string $host 43 | * @param int $port 44 | */ 45 | public function __construct($host, $port) { 46 | $this->host = $host; 47 | $this->port = $port; 48 | } 49 | 50 | /** 51 | * Method for indexing a new document 52 | * 53 | * @param array|object $document 54 | * @param mixed $id 55 | * @param array $options 56 | */ 57 | abstract public function index($document, $id=false, array $options = array()); 58 | 59 | /** 60 | * Method for updating a document 61 | * 62 | * @param array|object $partialDocument 63 | * @param mixed $id 64 | * @param array $options 65 | */ 66 | abstract public function update($partialDocument, $id, array $options = array()); 67 | 68 | /** 69 | * Perform a request against the given path/method/payload combination 70 | * Example: 71 | * $es->request('/_status'); 72 | * 73 | * @param string|array $path 74 | * @param string $method 75 | * @param array|bool $payload 76 | * @return 77 | */ 78 | abstract public function request($path, $method="GET", $payload=false); 79 | 80 | /** 81 | * Delete a document by its id 82 | * @param mixed $id 83 | */ 84 | abstract public function delete($id=false); 85 | 86 | /** 87 | * Perform a search based on query 88 | * @param array|string $query 89 | */ 90 | abstract public function search($query); 91 | 92 | /** 93 | * Search 94 | * 95 | * @return array 96 | * @param mixed $query String or array to use as criteria for delete 97 | * @param array $options Parameters to pass to delete action 98 | * @throws \Elasticsearch\Exception 99 | */ 100 | public function deleteByQuery($query, array $options = array()) { 101 | throw new \Elasticsearch\Exception(__FUNCTION__ . ' not implemented for ' . __CLASS__); 102 | } 103 | 104 | /** 105 | * Set what index to act against 106 | * @param string $index 107 | */ 108 | public function setIndex($index) { 109 | $this->index = $index; 110 | } 111 | 112 | /** 113 | * Set what document types to act against 114 | * @param string $type 115 | */ 116 | public function setType($type) { 117 | $this->type = $type; 118 | } 119 | 120 | /** 121 | * Build a callable url 122 | * 123 | * @return string 124 | * @param array|bool $path 125 | * @param array $options Query parameter options to pass 126 | */ 127 | protected function buildUrl($path = false, array $options = array()) { 128 | $isAbsolute = (is_array($path) ? $path[0][0] : $path[0]) === '/'; 129 | $url = $isAbsolute || null === $this->index ? '' : "/" . $this->index; 130 | 131 | if ($path && is_array($path) && count($path) > 0) { 132 | $path = implode("/", array_filter($path)); 133 | $url .= "/" . ltrim($path, '/'); 134 | } 135 | if (substr($url, -1) === "/") { 136 | $url = substr($url, 0, -1); 137 | } 138 | if (count($options) > 0) { 139 | $url .= "?" . http_build_query($options, '', '&'); 140 | } 141 | 142 | return $url; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /README.markdown: -------------------------------------------------------------------------------- 1 | [![Build Status](https://secure.travis-ci.org/nervetattoo/elasticsearch.png?branch=master)](http://travis-ci.org/nervetattoo/elasticsearch) 2 | # ElasticSearch PHP client 3 | ElasticSearch is a distributed lucene powered search indexing, this is a PHP client for it 4 | 5 | ## Usage 6 | 7 | ### Initial setup 8 | 9 | 1. Install composer. `curl -s http://getcomposer.org/installer | php` 10 | 2. Create `composer.json` containing: 11 | 12 | ```js 13 | { 14 | "require" : { 15 | "nervetattoo/elasticsearch" : ">=2.0" 16 | } 17 | } 18 | ``` 19 | 3. Run `./composer.phar install` 20 | 4. Keep up-to-date: `./composer.phar update` 21 | 22 | ### Indexing and searching 23 | 24 | ```php 25 | require_once __DIR__ . '/vendor/autoload.php'; 26 | 27 | use \ElasticSearch\Client; 28 | // The recommended way to go about things is to use an environment variable called ELASTICSEARCH_URL 29 | $es = Client::connection(); 30 | 31 | // Alternatively you can use dsn string 32 | $es = Client::connection('http://127.0.0.1:9200/myindex/mytype'); 33 | 34 | $es->index(array('title' => 'My cool document'), $id); 35 | $es->get($id); 36 | $es->search('title:cool'); 37 | ``` 38 | 39 | ### Creating mapping 40 | 41 | ```php 42 | $es->map(array( 43 | 'title' => array( 44 | 'type' => 'string', 45 | 'index' => 'analyzed' 46 | ) 47 | )); 48 | ``` 49 | 50 | ### Search multiple indexes or types 51 | 52 | ```php 53 | $results = $es 54 | ->setIndex(array("one", "two")) 55 | ->setType(array("mytype", "other-type")) 56 | ->search('title:cool'); 57 | ``` 58 | 59 | ### Using the Query DSL 60 | 61 | ```php 62 | $es->search(array( 63 | 'query' => array( 64 | 'term' => array('title' => 'cool') 65 | ) 66 | ); 67 | ``` 68 | 69 | ### Provide configuration as array 70 | 71 | Using an array for configuration also works 72 | 73 | ```php 74 | $es = Client::connection(array( 75 | 'servers' => '127.0.0.1:9200', 76 | 'protocol' => 'http', 77 | 'index' => 'myindex', 78 | 'type' => 'mytype' 79 | )); 80 | ``` 81 | 82 | ### Support for Routing 83 | 84 | ```php 85 | $document = array( 86 | 'title' => 'My routed document', 87 | 'user_id' => '42' 88 | ); 89 | $es->index($document, $id, array('routing' => $document['user_id'])); 90 | $es->search('title:routed', array('routing' => '42')); 91 | ``` 92 | 93 | 94 | ### Support for Bulking 95 | 96 | ```php 97 | $document = array( 98 | 'title' => 'My bulked entry', 99 | 'user_id' => '43' 100 | ); 101 | $es->beginBulk(); 102 | $es->index($document, $id, array('routing' => $document['user_id'])); 103 | $es->delete(2); 104 | $es->delete(3); 105 | $es->commitBulk(); 106 | 107 | 108 | $es->createBulk() 109 | ->delete(4) 110 | ->index($document, $id, 'myIndex', 'myType', array('parent' => $parentId)); 111 | ->delete(5) 112 | ->delete(6) 113 | ->commit(); 114 | 115 | ``` 116 | 117 | ### Usage as a service in Symfony2 118 | 119 | In order to use the Dependency Injection to inject the client as a service, you'll have to define it before. 120 | So in your bundle's services.yml file you can put something like this : 121 | ```yml 122 | your_bundle.elastic_transport: 123 | class: ElasticSearch\Transport\HTTP 124 | arguments: 125 | - localhost 126 | - 9200 127 | - 60 128 | 129 | your_bundle.elastic_client: 130 | class: ElasticSearch\Client 131 | arguments: 132 | - @your_bundle.elastic_transport 133 | ``` 134 | To make Symfony2 recognize the `ElasticSearch` namespace, you'll have to register it. So in your `app/autoload.php` make sure your have : 135 | ```php 136 | // ... 137 | 138 | $loader->registerNamespaces(array( 139 | // ... 140 | 'ElasticSearch' => __DIR__.'/path/to/your/vendor/nervetattoo/elasticsearch/src', 141 | )); 142 | ``` 143 | Then, you can get your client via the service container and use it like usual. For example, in your controller you can do this : 144 | ```php 145 | class FooController extends Controller 146 | { 147 | // ... 148 | 149 | public function barAction() 150 | { 151 | // ... 152 | $es = $this->get('your_bundle.elastic_client'); 153 | $results = $es 154 | ->setIndex(array("one", "two")) 155 | ->setType(array("mytype", "other-type")) 156 | ->search('title:cool'); 157 | } 158 | } 159 | ``` 160 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /src/ElasticSearch/Transport/Memcached.php: -------------------------------------------------------------------------------- 1 | 12 | * 13 | * For the full copyright and license information, please view the LICENSE 14 | * file that was distributed with this source code. 15 | */ 16 | 17 | class Memcached extends Base { 18 | public function __construct($host="127.0.0.1", $port=11311, $timeout=null) { 19 | parent::__construct($host, $port); 20 | $this->conn = new Memcache; 21 | $this->conn->connect($host, $port, $timeout); 22 | } 23 | 24 | /** 25 | * Index a new document or update it if existing 26 | * 27 | * @return array 28 | * @param array $document 29 | * @param mixed $id Optional 30 | * @param array $options 31 | * @throws \ElasticSearch\Exception 32 | */ 33 | public function index($document, $id=false, array $options = array()) { 34 | if ($id === false) 35 | throw new \ElasticSearch\Exception("Memcached transport requires id when indexing"); 36 | 37 | $document = json_encode($document); 38 | $url = $this->buildUrl(array($this->type, $id)); 39 | $response = $this->conn->set($url, $document); 40 | return array( 41 | 'ok' => $response 42 | ); 43 | } 44 | 45 | /** 46 | * Update a part of a document 47 | * 48 | * @return array 49 | * @param array $partialDocument 50 | * @param mixed $id 51 | * @param array $options 52 | */ 53 | public function update($partialDocument, $id, array $options = array()) { 54 | $document = json_encode(array('doc' => $partialDocument)); 55 | $url = $this->buildUrl(array($this->type, $id)); 56 | $response = $this->conn->set($url, $document); 57 | 58 | return array( 59 | 'ok' => $response, 60 | ); 61 | } 62 | 63 | /** 64 | * Search 65 | * 66 | * @return array 67 | * @param array|string $query 68 | * @throws \ElasticSearch\Exception 69 | */ 70 | public function search($query) { 71 | if (is_array($query)) { 72 | if (array_key_exists("query", $query)) { 73 | $dsl = new Stringify($query); 74 | $q = (string) $dsl; 75 | $url = $this->buildUrl(array( 76 | $this->type, "_search?q=" . $q 77 | )); 78 | $result = json_decode($this->conn->get($url), true); 79 | return $result; 80 | } 81 | throw new \ElasticSearch\Exception("Memcached protocol doesnt support the full DSL, only query"); 82 | } 83 | elseif (is_string($query)) { 84 | /** 85 | * String based search means http query string search 86 | */ 87 | $url = $this->buildUrl(array( 88 | $this->type, "_search?q=" . $query 89 | )); 90 | $result = json_decode($this->conn->get($url), true); 91 | return $result; 92 | } 93 | } 94 | 95 | /** 96 | * Perform a request against the given path/method/payload combination 97 | * Example: 98 | * $es->request('/_status'); 99 | * 100 | * @param string|array $path 101 | * @param string $method 102 | * @param array|bool $payload 103 | * @return array 104 | */ 105 | public function request($path, $method="GET", $payload=false) { 106 | $url = $this->buildUrl($path); 107 | switch ($method) { 108 | case 'GET': 109 | $result = $this->conn->get($url); 110 | break; 111 | case 'DELETE': 112 | $result = $this->conn->delete($url); 113 | break; 114 | } 115 | return json_decode($result); 116 | } 117 | 118 | /** 119 | * Flush this index/type combination 120 | * 121 | * @return array 122 | * @param mixed $id 123 | * @param array $options Parameters to pass to delete action 124 | */ 125 | public function delete($id=false, array $options = array()) { 126 | if ($id) 127 | return $this->request(array($this->type, $id), "DELETE"); 128 | else 129 | return $this->request(false, "DELETE"); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /docs/dsl_spec: -------------------------------------------------------------------------------- 1 | From: http://groups.google.com/a/elasticsearch.com/group/users/browse_thread/thread/549fb5ede5df6ff4/0890e504cc13d486 2 | 3 | QUERY DSL SPEC: (better read in fixed-width font) 4 | -------------------------------------------------- 5 | 6 | curl -XGET 'http://$server/_search -d '{ TOPLEVEL }' 7 | curl -XGET 'http://$server/_all/_search -d '{ TOPLEVEL }' 8 | curl -XGET 'http://$server/_all/$types/_search -d '{ TOPLEVEL }' 9 | curl -XGET 'http://$server/$indices/_search -d '{ TOPLEVEL }' 10 | curl -XGET 'http://$server/$indices/$types/_search -d '{ TOPLEVEL }' 11 | 12 | TOPLEVEL: 13 | --------- 14 | { 15 | explain: BOOL, 16 | from: INT, 17 | size: INT, 18 | fields: ["field_1", "field_n"], 19 | query: { QUERY_CLAUSE }, 20 | facets: { FACETS_CLAUSE }, 21 | sort: { SORT_CLAUSE } 22 | } 23 | 24 | QUERY_CLAUSE: 25 | ------------- 26 | { 27 | term: { TERM_QUERY } 28 | | range: { RANGE_QUERY } 29 | | prefix: { PREFIX_QUERY } 30 | | wildcard: { WILDCARD_QUERY } 31 | | matchAll: { MATCH_ALL_QUERY } 32 | | queryString: { QUERY_STRING_QUERY } 33 | | bool: { BOOLEAN_QUERY } 34 | | disMax: { DISMAX_QUERY } 35 | | constantScore: { CONSTANT_SCORE_QUERY } 36 | | filteredQuery: { FILTERED_QUERY }, 37 | } 38 | 39 | FILTER_CLAUSE: 40 | -------------- 41 | { 42 | query: { QUERY_CLAUSE }, 43 | | term: { TERM_FILTER }, 44 | | range: { RANGE_FILTER }, 45 | | prefix: { PREFIX_FILTER }, 46 | | wildcard: { WILDCARD_FILTER }, 47 | | bool: { BOOLEAN_FILTER }, 48 | | constantScore: { CONSTANT_SCORE_QUERY } 49 | } 50 | 51 | FACETS_CLAUSE: 52 | -------------- 53 | { 54 | $facet_name_1: { QUERY_CLAUSE }, 55 | $facet_name_n: ... 56 | } 57 | 58 | SORT_CLAUSE: 59 | ------------ 60 | [ 61 | $fieldname_1 | "score 62 | | { $fieldname_1 : { reverse: BOOL }}, 63 | ... 64 | ] 65 | 66 | TERM_FILTER: 67 | ------------ 68 | { $fieldname: VALUE_1 } 69 | 70 | TERM_QUERY: 71 | ----------- 72 | { $fieldname: VALUE_1 } 73 | | { $fieldname: { value: VALUE, boost: FLOAT } } 74 | 75 | PREFIX_FILTER: 76 | -------------- 77 | { $fieldname: STRING} 78 | 79 | PREFIX_QUERY: 80 | ------------- 81 | { $fieldname: STRING} 82 | | { $fieldname: { prefix: STRING_1, boost: FLOAT } } 83 | 84 | WILDCARD_FILTER: 85 | ---------------- 86 | { $fieldname: STRING } 87 | 88 | WILDCARD_QUERY: 89 | --------------- 90 | { $fieldname: STRING } 91 | | { $fieldname: { wildcard: STRING, boost: FLOAT } } 92 | 93 | MATCH_ALL_QUERY: 94 | ---------------- 95 | {} 96 | | { boost: FLOAT } 97 | 98 | RANGE_FILTER: 99 | ------------- 100 | { $fieldname: { 101 | from: INT | FLOAT | STRING | DATETIME, 102 | to: INT | FLOAT | STRING | DATETIME, 103 | includeLower: BOOL, 104 | includeUpper: BOOL, 105 | }} 106 | 107 | RANGE_QUERY: 108 | ------------ 109 | { $fieldname: { 110 | from: INT | FLOAT | STRING | DATETIME, 111 | to: INT | FLOAT | STRING | DATETIME, 112 | includeLower: BOOL 113 | includeUpper: BOOL 114 | boost: FLOAT 115 | }} 116 | 117 | BOOLEAN_FILTER: 118 | --------------- 119 | { 120 | must: { FILTER_CLAUSE } | [ { FILTER_CLAUSE }, ... ], 121 | should: { FILTER_CLAUSE } | [ { FILTER_CLAUSE }, ... ], 122 | mustNot: { FILTER_CLAUSE } | [ { FILTER_CLAUSE }, ... ], 123 | 124 | minimumNumberShouldMatch: INT 125 | } 126 | 127 | BOOLEAN_QUERY: 128 | -------------- 129 | { 130 | must: { QUERY_CLAUSE} | [ { QUERY_CLAUSE }, ... ], 131 | should: { QUERY_CLAUSE} | [ { QUERY_CLAUSE }, ... ], 132 | mustNot: { QUERY_CLAUSE} | [ { QUERY_CLAUSE }, ... ], 133 | 134 | boost: FLOAT, 135 | minimumNumberShouldMatch: INT 136 | } 137 | 138 | DISMAX_QUERY: 139 | ------------- 140 | { 141 | queries: [ { QUERY_CLAUSE }, ... ], 142 | tieBreakerMultiplier: FLOAT, 143 | boost: FLOAT 144 | } 145 | 146 | CONSTANT_SCORE_QUERY: 147 | --------------------- 148 | { 149 | filter: { FILTER_CLAUSE } 150 | boost: FLOAT 151 | } 152 | 153 | FILTERED_QUERY: 154 | --------------- 155 | { 156 | query: { QUERY_CLAUSE }, 157 | filter: { FILTER_CLAUSE, ... } 158 | } 159 | 160 | QUERY_STRING_QUERY: 161 | ------------------- 162 | { 163 | query: STRING, 164 | defaultField: $fieldname, 165 | defaultOperator: "AND" | "OR", 166 | analyzer: STRING, 167 | allowLeadingWildcard: BOOL, 168 | lowercaseExpandedTerms: BOOL, 169 | enablePositionIncrements: BOOL, 170 | fuzzyPrefixLength: BOOL, 171 | fuzzyMinSim: FLOAT, 172 | phraseSlop: INT, 173 | boost: FLOAT 174 | } 175 | 176 | ARGUMENT TYPES: 177 | --------------- 178 | - BOOL: true | false 179 | - INT: integer eg 5 180 | - FLOAT: float eg 1.2 181 | - STRING: text eg "foo" 182 | - DATETIME: dates and times 183 | eg "2010-02-31T13:30:45", "2010-02-31", "13:30:45" 184 | - VALUE: BOOL | INT | FLOAT | STRING | DATETIME 185 | -------------------------------------------------------------------------------- /src/ElasticSearch/Transport/HTTP.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | if (!defined('CURLE_OPERATION_TIMEDOUT')) 15 | define('CURLE_OPERATION_TIMEDOUT', 28); 16 | 17 | 18 | class HTTP extends Base { 19 | 20 | /** 21 | * How long before timing out CURL call 22 | */ 23 | private $timeout = 5; 24 | 25 | /** 26 | * curl handler which is needed for reusing existing http connection to the server 27 | * @var resource 28 | */ 29 | protected $ch; 30 | 31 | 32 | public function __construct($host='localhost', $port=9200, $timeout=null) { 33 | parent::__construct($host, $port); 34 | if(null !== $timeout) { 35 | $this->setTimeout($timeout); 36 | } 37 | $this->ch = curl_init(); 38 | } 39 | 40 | /** 41 | * Index a new document or update it if existing 42 | * 43 | * @return array 44 | * @param array $document 45 | * @param mixed $id Optional 46 | * @param array $options 47 | */ 48 | public function index($document, $id=false, array $options = array()) { 49 | $url = $this->buildUrl(array($this->type, $id), $options); 50 | $method = ($id == false) ? "POST" : "PUT"; 51 | return $this->call($url, $method, $document); 52 | } 53 | 54 | /** 55 | * Update a part of a document 56 | * 57 | * @return array 58 | * 59 | * @param array $partialDocument 60 | * @param mixed $id 61 | * @param array $options 62 | */ 63 | public function update($partialDocument, $id, array $options = array()) { 64 | $url = $this->buildUrl(array($this->type, $id, '_update'), $options); 65 | 66 | return $this->call($url, "POST", array('doc' => $partialDocument)); 67 | } 68 | 69 | /** 70 | * Search 71 | * 72 | * @return array 73 | * @param array|string $query 74 | * @param array $options 75 | */ 76 | public function search($query, array $options = array()) { 77 | $result = false; 78 | if (is_array($query)) { 79 | /** 80 | * Array implies using the JSON query DSL 81 | */ 82 | $arg = "_search"; 83 | /** 84 | * $options may contain values like: 85 | * $options['routing'] = 'user123' 86 | * or 87 | * $options['preference'] = 'xyzabc123' 88 | */ 89 | $url = $this->buildUrl(array($this->type, $arg), $options); 90 | 91 | $result = $this->call($url, "GET", $query); 92 | } 93 | elseif (is_string($query)) { 94 | /** 95 | * String based search means http query string search 96 | */ 97 | $url = $this->buildUrl(array( 98 | $this->type, "_search?q=" . $query 99 | )); 100 | $result = $this->call($url, "POST", $options); 101 | } 102 | else { 103 | /** 104 | * no http query string search 105 | */ 106 | $url = $this->buildUrl(array( 107 | $this->type, "_search?" 108 | )); 109 | $result = $this->call($url, "POST", $options); 110 | } 111 | return $result; 112 | } 113 | 114 | /** 115 | * Search 116 | * 117 | * @return array 118 | * @param mixed $query 119 | * @param array $options Parameters to pass to delete action 120 | */ 121 | public function deleteByQuery($query, array $options = array()) { 122 | $options += array( 123 | 'refresh' => true 124 | ); 125 | if (is_array($query)) { 126 | /** 127 | * Array implies using the JSON query DSL 128 | */ 129 | $url = $this->buildUrl(array($this->type, "_query")); 130 | $result = $this->call($url, "DELETE", $query); 131 | } 132 | elseif (is_string($query)) { 133 | /** 134 | * String based search means http query string search 135 | */ 136 | $url = $this->buildUrl(array($this->type, "_query"), array('q' => $query)); 137 | $result = $this->call($url, "DELETE"); 138 | } 139 | if ($options['refresh']) { 140 | $this->request('_refresh', "POST"); 141 | } 142 | return !isset($result['error']); 143 | } 144 | 145 | /** 146 | * Perform a request against the given path/method/payload combination 147 | * Example: 148 | * $es->request('/_status'); 149 | * 150 | * @param string|array $path 151 | * @param string $method 152 | * @param array|bool $payload 153 | * @return array 154 | */ 155 | public function request($path, $method="GET", $payload=false) { 156 | return $this->call($this->buildUrl($path), $method, $payload); 157 | } 158 | 159 | /** 160 | * Flush this index/type combination 161 | * 162 | * @return array 163 | * @param mixed $id Id of document to delete 164 | * @param array $options Parameters to pass to delete action 165 | */ 166 | public function delete($id=false, array $options = array()) { 167 | if ($id) 168 | return $this->call($this->buildUrl(array($this->type, $id), $options), "DELETE"); 169 | else 170 | return $this->request(false, "DELETE"); 171 | } 172 | 173 | /** 174 | * Perform a http call against an url with an optional payload 175 | * 176 | * @return array 177 | * @param string $url 178 | * @param string $method (GET/POST/PUT/DELETE) 179 | * @param array|bool $payload The document/instructions to pass along 180 | * @throws HTTPException 181 | */ 182 | protected function call($url, $method="GET", $payload=null) { 183 | $conn = $this->ch; 184 | $protocol = "http"; 185 | $requestURL = $protocol . "://" . $this->host . $url; 186 | curl_setopt($conn, CURLOPT_URL, $requestURL); 187 | curl_setopt($conn, CURLOPT_TIMEOUT, $this->timeout); 188 | curl_setopt($conn, CURLOPT_PORT, $this->port); 189 | curl_setopt($conn, CURLOPT_CUSTOMREQUEST, strtoupper($method)); 190 | curl_setopt($conn, CURLOPT_FORBID_REUSE , 0) ; 191 | 192 | $headers = array(); 193 | $headers[] = 'Accept: application/json'; 194 | $headers[] = 'Content-Type: application/json'; 195 | 196 | curl_setopt($conn, CURLOPT_HTTPHEADER, $headers); 197 | 198 | if (is_array($payload) && count($payload) > 0) 199 | curl_setopt($conn, CURLOPT_POSTFIELDS, json_encode($payload)) ; 200 | else 201 | curl_setopt($conn, CURLOPT_POSTFIELDS, $payload); 202 | 203 | // cURL opt returntransfer leaks memory, therefore OB instead. 204 | ob_start(); 205 | curl_exec($conn); 206 | $response = ob_get_clean(); 207 | if ($response !== false) { 208 | $data = json_decode($response, true); 209 | if (!$data) { 210 | $data = array('error' => $response, "code" => curl_getinfo($conn, CURLINFO_HTTP_CODE)); 211 | } 212 | } 213 | else { 214 | /** 215 | * cUrl error code reference can be found here: 216 | * http://curl.haxx.se/libcurl/c/libcurl-errors.html 217 | */ 218 | $errno = curl_errno($conn); 219 | switch ($errno) 220 | { 221 | case CURLE_UNSUPPORTED_PROTOCOL: 222 | $error = "Unsupported protocol [$protocol]"; 223 | break; 224 | case CURLE_FAILED_INIT: 225 | $error = "Internal cUrl error?"; 226 | break; 227 | case CURLE_URL_MALFORMAT: 228 | $error = "Malformed URL [$requestURL] -d " . json_encode($payload); 229 | break; 230 | case CURLE_COULDNT_RESOLVE_PROXY: 231 | $error = "Couldnt resolve proxy"; 232 | break; 233 | case CURLE_COULDNT_RESOLVE_HOST: 234 | $error = "Couldnt resolve host"; 235 | break; 236 | case CURLE_COULDNT_CONNECT: 237 | $error = "Couldnt connect to host [{$this->host}], ElasticSearch down?"; 238 | break; 239 | case CURLE_OPERATION_TIMEDOUT: 240 | $error = "Operation timed out on [$requestURL]"; 241 | break; 242 | default: 243 | $error = "Unknown error"; 244 | if ($errno == 0) { 245 | $error .= ". Non-cUrl error"; 246 | } else { 247 | $errstr = curl_error($conn); 248 | $error .= " ($errstr)"; 249 | } 250 | break; 251 | } 252 | $exception = new HTTPException($error); 253 | $exception->payload = $payload; 254 | $exception->port = $this->port; 255 | $exception->protocol = $protocol; 256 | $exception->host = $this->host; 257 | $exception->method = $method; 258 | throw $exception; 259 | } 260 | 261 | return $data; 262 | } 263 | 264 | public function setTimeout($timeout) 265 | { 266 | $this->timeout = $timeout; 267 | } 268 | 269 | public function getTimeout() 270 | { 271 | return $this->timeout; 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/ElasticSearch/Client.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | class Client { 15 | const DEFAULT_PROTOCOL = 'http'; 16 | const DEFAULT_SERVER = '127.0.0.1:9200'; 17 | const DEFAULT_INDEX = 'default-index'; 18 | const DEFAULT_TYPE = 'default-type'; 19 | 20 | protected $_config = array(); 21 | 22 | protected static $_defaults = array( 23 | 'protocol' => Client::DEFAULT_PROTOCOL, 24 | 'servers' => Client::DEFAULT_SERVER, 25 | 'index' => Client::DEFAULT_INDEX, 26 | 'type' => Client::DEFAULT_TYPE, 27 | 'timeout' => null, 28 | ); 29 | 30 | protected static $_protocols = array( 31 | 'http' => 'ElasticSearch\\Transport\\HTTP', 32 | 'memcached' => 'ElasticSearch\\Transport\\Memcached', 33 | ); 34 | 35 | private $transport, $index, $type, $bulk; 36 | 37 | /** 38 | * Construct search client 39 | * 40 | * @return \ElasticSearch\Client 41 | * @param \ElasticSearch\Transport\Base $transport 42 | * @param string $index 43 | * @param string $type 44 | */ 45 | public function __construct($transport, $index = null, $type = null) { 46 | $this->transport = $transport; 47 | $this->setIndex($index)->setType($type); 48 | } 49 | 50 | /** 51 | * Get a client instance 52 | * Defaults to opening a http transport connection to 127.0.0.1:9200 53 | * 54 | * @param string|array $config Allow overriding only the configuration bits you desire 55 | * - _transport_ 56 | * - _host_ 57 | * - _port_ 58 | * - _index_ 59 | * - _type_ 60 | * @throws \Exception 61 | * @return \ElasticSearch\Client 62 | */ 63 | public static function connection($config = array()) { 64 | if (!$config && ($url = getenv('ELASTICSEARCH_URL'))) { 65 | $config = $url; 66 | } 67 | if (is_string($config)) { 68 | $config = self::parseDsn($config); 69 | } 70 | 71 | $config += self::$_defaults; 72 | 73 | $protocol = $config['protocol']; 74 | if (!isset(self::$_protocols[$protocol])) { 75 | throw new \Exception("Tried to use unknown protocol: $protocol"); 76 | } 77 | $class = self::$_protocols[$protocol]; 78 | 79 | if (null !== $config['timeout'] && !is_numeric($config['timeout'])) { 80 | throw new \Exception("HTTP timeout should have a numeric value when specified."); 81 | } 82 | 83 | $server = is_array($config['servers']) ? $config['servers'][0] : $config['servers']; 84 | list($host, $port) = explode(':', $server); 85 | 86 | $transport = new $class($host, $port, $config['timeout']); 87 | 88 | $client = new self($transport, $config['index'], $config['type']); 89 | $client->config($config); 90 | return $client; 91 | } 92 | 93 | /** 94 | * @param array|null $config 95 | * @return array|void 96 | */ 97 | public function config($config = null) { 98 | if (!$config) 99 | return $this->_config; 100 | if (is_array($config)) 101 | $this->_config = $config + $this->_config; 102 | } 103 | 104 | /** 105 | * Change what index to go against 106 | * @return \ElasticSearch\Client 107 | * @param mixed $index 108 | */ 109 | public function setIndex($index) { 110 | if (is_array($index)) 111 | $index = implode(",", array_filter($index)); 112 | $this->index = $index; 113 | $this->transport->setIndex($index); 114 | return $this; 115 | } 116 | 117 | /** 118 | * Change what types to act against 119 | * @return \ElasticSearch\Client 120 | * @param mixed $type 121 | */ 122 | public function setType($type) { 123 | if (is_array($type)) 124 | $type = implode(",", array_filter($type)); 125 | $this->type = $type; 126 | $this->transport->setType($type); 127 | return $this; 128 | } 129 | 130 | /** 131 | * Fetch a document by its id 132 | * 133 | * @return array 134 | * @param mixed $id Optional 135 | * @param bool $verbose 136 | */ 137 | public function get($id, $verbose=false) { 138 | return $this->request($id, "GET"); 139 | } 140 | 141 | /** 142 | * Puts a mapping on index 143 | * 144 | * @param array|object $mapping 145 | * @param array $config 146 | * @throws Exception 147 | * @return array 148 | */ 149 | public function map($mapping, array $config = array()) { 150 | if (is_array($mapping)) $mapping = new Mapping($mapping); 151 | $mapping->config($config); 152 | 153 | try { 154 | $type = $mapping->config('type'); 155 | } 156 | catch (\Exception $e) {} // No type is cool 157 | if (isset($type) && !$this->passesTypeConstraint($type)) { 158 | throw new Exception("Cant create mapping due to type constraint mismatch"); 159 | } 160 | 161 | return $this->request('_mapping', 'PUT', $mapping->export(), true); 162 | } 163 | 164 | protected function passesTypeConstraint($constraint) { 165 | if (is_string($constraint)) $constraint = array($constraint); 166 | $currentType = explode(',', $this->type); 167 | $includeTypes = array_intersect($constraint, $currentType); 168 | return ($constraint && count($includeTypes) === count($constraint)); 169 | } 170 | 171 | /** 172 | * Perform a raw request 173 | * 174 | * Usage example 175 | * 176 | * $response = $client->request('_status', 'GET'); 177 | * 178 | * @return array 179 | * @param mixed $path Request path to use. 180 | * `type` is prepended to this path inside request 181 | * @param string $method HTTP verb to use 182 | * @param mixed $payload Array of data to be json-encoded 183 | * @param bool $verbose Controls response data, if `false` 184 | * only `_source` of response is returned 185 | */ 186 | public function request($path, $method = 'GET', $payload = false, $verbose=false) { 187 | $response = $this->transport->request($this->expandPath($path), $method, $payload); 188 | return ($verbose || !isset($response['_source'])) 189 | ? $response 190 | : $response['_source']; 191 | } 192 | 193 | /** 194 | * Index a new document or update it if existing 195 | * 196 | * @return array 197 | * @param array $document 198 | * @param mixed $id Optional 199 | * @param array $options Allow sending query parameters to control indexing further 200 | * _refresh_ *bool* If set to true, immediately refresh the shard after indexing 201 | */ 202 | public function index($document, $id=false, array $options = array()) { 203 | if ($this->bulk) { 204 | return $this->bulk->index($document, $id, $this->index, $this->type, $options); 205 | } 206 | return $this->transport->index($document, $id, $options); 207 | } 208 | 209 | /** 210 | * Update a part of a document 211 | * 212 | * @return array 213 | * 214 | * @param array $partialDocument 215 | * @param mixed $id 216 | * @param array $options Allow sending query parameters to control indexing further 217 | * _refresh_ *bool* If set to true, immediately refresh the shard after indexing 218 | */ 219 | public function update($partialDocument, $id, array $options = array()) { 220 | if ($this->bulk) { 221 | return $this->bulk->update($partialDocument, $id, $this->index, $this->type, $options); 222 | } 223 | return $this->transport->update($partialDocument, $id, $options); 224 | } 225 | 226 | /** 227 | * Perform search, this is the sweet spot 228 | * 229 | * @return array 230 | * @param $query 231 | * @param array $options 232 | */ 233 | public function search($query, array $options = array()) { 234 | $start = microtime(true); 235 | $result = $this->transport->search($query, $options); 236 | $result['time'] = microtime(true) - $start; 237 | return $result; 238 | } 239 | 240 | /** 241 | * Flush this index/type combination 242 | * 243 | * @return array 244 | * @param mixed $id If id is supplied, delete that id for this index 245 | * if not wipe the entire index 246 | * @param array $options Parameters to pass to delete action 247 | */ 248 | public function delete($id=false, array $options = array()) { 249 | if ($this->bulk) { 250 | return $this->bulk->delete($id, $this->index, $this->type, $options); 251 | } 252 | return $this->transport->delete($id, $options); 253 | } 254 | 255 | /** 256 | * Flush this index/type combination 257 | * 258 | * @return array 259 | * @param mixed $query Text or array based query to delete everything that matches 260 | * @param array $options Parameters to pass to delete action 261 | */ 262 | public function deleteByQuery($query, array $options = array()) { 263 | return $this->transport->deleteByQuery($query, $options); 264 | } 265 | 266 | /** 267 | * Perform refresh of current indexes 268 | * 269 | * @return array 270 | */ 271 | public function refresh() { 272 | return $this->transport->request(array('_refresh'), 'GET'); 273 | } 274 | 275 | /** 276 | * Expand a given path (array or string) 277 | * If this is not an absolute path index + type will be prepended 278 | * If it is an absolute path it will be used as is 279 | * 280 | * @param mixed $path 281 | * @return array 282 | */ 283 | protected function expandPath($path) { 284 | $path = (array) $path; 285 | $isAbsolute = $path[0][0] === '/'; 286 | 287 | return $isAbsolute 288 | ? $path 289 | : array_merge((array) $this->type, $path); 290 | } 291 | 292 | /** 293 | * Parse a DSN string into an associative array 294 | * 295 | * @param string $dsn 296 | * @return array 297 | */ 298 | protected static function parseDsn($dsn) { 299 | $parts = parse_url($dsn); 300 | $protocol = $parts['scheme']; 301 | $servers = $parts['host'] . ':' . $parts['port']; 302 | if (isset($parts['path'])) { 303 | $path = explode('/', $parts['path']); 304 | list($index, $type) = array_values(array_filter($path)); 305 | } 306 | return compact('protocol', 'servers', 'index', 'type'); 307 | } 308 | 309 | /** 310 | * Create a bulk-transaction 311 | * 312 | * @return \Elasticsearch\Bulk 313 | */ 314 | 315 | public function createBulk() { 316 | return new Bulk($this); 317 | } 318 | 319 | 320 | /** 321 | * Begin a transparent bulk-transaction 322 | * if one is already running, return its handle 323 | * @return \Elasticsearch\Bulk 324 | */ 325 | 326 | public function beginBulk() { 327 | if (!$this->bulk) { 328 | $this->bulk = $this->createBulk($this); 329 | } 330 | return $this->bulk; 331 | } 332 | 333 | /** 334 | * @see beginBulk 335 | */ 336 | public function begin() { 337 | return $this->beginBulk(); 338 | } 339 | 340 | /** 341 | * commit a bulk-transaction 342 | * @return array 343 | */ 344 | 345 | public function commitBulk() { 346 | if ($this->bulk && $this->bulk->count()) { 347 | $result = $this->bulk->commit(); 348 | $this->bulk = null; 349 | return $result; 350 | } 351 | } 352 | 353 | /** 354 | * @see commitBulk 355 | */ 356 | public function commit() { 357 | return $this->commitBulk(); 358 | } 359 | 360 | } 361 | -------------------------------------------------------------------------------- /tests/units/Client.php: -------------------------------------------------------------------------------- 1 | setIndex('index')->delete(); 12 | \ElasticSearch\Client::connection()->setIndex('index2')->delete(); 13 | \ElasticSearch\Client::connection()->setIndex('test-index')->delete(); 14 | \ElasticSearch\Client::connection()->delete(); 15 | } 16 | 17 | public function testDsnIsCorrectlyParsed() { 18 | $search = \ElasticSearch\Client::connection('http://test.com:9100/index/type'); 19 | $config = array( 20 | 'protocol' => 'http', 21 | 'servers' => 'test.com:9100', 22 | 'index' => 'index', 23 | 'type' => 'type', 24 | 'timeout' => null, 25 | ); 26 | $this->assert->array($search->config()) 27 | ->isEqualTo($config); 28 | } 29 | 30 | public function testAbsoluteRequest() { 31 | $client = \ElasticSearch\Client::connection(); 32 | $resp = $client->request('/'); 33 | $this->assert->array($resp) 34 | ->integer($resp['status'])->isEqualTo(200) 35 | ->string($resp['tagline'])->isEqualTo('You Know, for Search'); 36 | } 37 | 38 | /** 39 | * Test indexing a new document 40 | */ 41 | public function testIndexingDocument() { 42 | $tag = $this->getTag(); 43 | $doc = array( 44 | 'title' => 'One cool ' . $tag 45 | ); 46 | $client = \ElasticSearch\Client::connection(); 47 | $resp = $client->index($doc, $tag, array('refresh' => true)); 48 | $this->assert->array($resp)->boolean($resp['created'])->isTrue(1); 49 | 50 | $fetchedDoc = $client->get($tag); 51 | $this->assert->array($fetchedDoc)->isEqualTo($doc); 52 | } 53 | 54 | /** 55 | * Test updating an existing document 56 | */ 57 | public function testUpdatingDocument() { 58 | $tag = $this->getTag(); 59 | $doc = array( 60 | 'title' => 'One cool ' . $tag, 61 | 'body' => 'Second cool ' . $tag, 62 | ); 63 | $client = \ElasticSearch\Client::connection(); 64 | $resp = $client->index($doc, $tag, array('refresh' => true)); 65 | $this->assert->array($resp)->boolean($resp['created'])->isTrue(); 66 | $this->assert->array($resp)->notHasKey('error'); 67 | 68 | $fetchedDoc = $client->get($tag); 69 | $this->assert->array($fetchedDoc)->isEqualTo($doc); 70 | 71 | $partialDoc = array( 72 | 'body' => 'Updated cool ' . $tag, 73 | ); 74 | $resp = $client->update($doc, $tag, array('refresh' => true)); 75 | $this->assert->array($resp)->boolean($resp['created'])->isFalse(); 76 | $this->assert->array($resp)->notHasKey('error'); 77 | } 78 | 79 | /** 80 | * Test regular string search 81 | */ 82 | public function testStringSearch() { 83 | $client = \ElasticSearch\Client::connection(); 84 | $tag = $this->getTag(); 85 | Helper::addDocuments($client, 3, $tag); 86 | $resp = $client->search("title:$tag"); 87 | $this->assert->array($resp)->hasKey('hits') 88 | ->array($resp['hits'])->hasKey('total') 89 | ->integer($resp['hits']['total'])->isEqualTo(3); 90 | } 91 | 92 | /** 93 | * Test indexing a new document and having an auto id 94 | * This means dupes will occur 95 | */ 96 | public function testIndexingDocumentWithoutId() { 97 | $doc = array( 98 | 'title' => 'One cool ' . $this->getTag() 99 | ); 100 | $client = \ElasticSearch\Client::connection(); 101 | $resp = $client->index($doc, false, array('refresh' => true)); 102 | $this->assert->array($resp) 103 | ->boolean($resp['created'])->isTrue(1); 104 | } 105 | 106 | /** 107 | * Test delete by query 108 | */ 109 | public function testDeleteByQuery() { 110 | $options = array('refresh' => true); 111 | $client = \ElasticSearch\Client::connection(); 112 | $word = $this->getTag(); 113 | $resp = $client->index(array('title' => $word), 1, $options); 114 | 115 | $client->refresh(); 116 | 117 | $del = $client->deleteByQuery(array( 118 | 'query' => array( 119 | 'term' => array('title' => $word) 120 | ) 121 | )); 122 | 123 | $hits = $client->search(array( 124 | 'query' => array( 125 | 'term' => array('title' => $word) 126 | ) 127 | )); 128 | $this->assert->array($hits)->hasKey('hits') 129 | ->array($hits['hits'])->hasKey('total') 130 | ->integer($hits['hits']['total'])->isEqualTo(0); 131 | } 132 | 133 | /** 134 | * Test a midly complex search 135 | */ 136 | public function testSlightlyComplexSearch() { 137 | $client = \ElasticSearch\Client::connection(); 138 | 139 | $uniqueWord = $this->getTag(); 140 | $docs = 3; 141 | $doc = array( 142 | 'title' => "One cool document $uniqueWord", 143 | 'tag' => array('cool', "stuff", "2k") 144 | ); 145 | while ($docs-- > 0) { 146 | $resp = $client->index($doc, false, array('refresh' => true)); 147 | } 148 | 149 | $hits = $client->search(array( 150 | 'query' => array( 151 | 'bool' => array( 152 | 'must' => array( 153 | 'term' => array('title' => $uniqueWord) 154 | ), 155 | 'should' => array( 156 | 'term' => array( 157 | 'tag' => 'stuff' 158 | ) 159 | ) 160 | ) 161 | ) 162 | )); 163 | 164 | $this->assert->array($hits)->hasKey('hits') 165 | ->array($hits['hits'])->hasKey('total') 166 | ->integer($hits['hits']['total'])->isEqualTo(3); 167 | } 168 | 169 | /** 170 | * Test multi index search 171 | */ 172 | public function testSearchMultipleIndexes() 173 | { 174 | $client = \ElasticSearch\Client::connection(); 175 | $tag = $this->getTag(); 176 | 177 | $primaryIndex = 'test-index'; 178 | $secondaryIndex = 'test-index2'; 179 | $doc = array('title' => $tag); 180 | $options = array('refresh' => true); 181 | $client->setIndex($secondaryIndex)->index($doc, false, $options); 182 | $client->setIndex($primaryIndex)->index($doc, false, $options); 183 | 184 | $indexes = array($primaryIndex, $secondaryIndex); 185 | 186 | // Use both indexes when searching 187 | $resp = $client->setIndex($indexes)->search("title:$tag"); 188 | 189 | $this->assert->array($resp)->hasKey('hits') 190 | ->array($resp['hits'])->hasKey('total') 191 | ->integer($resp['hits']['total'])->isEqualTo(2); 192 | 193 | $client->delete(); 194 | } 195 | 196 | /** 197 | * @expectedException ElasticSearch\Transport\HTTPException 198 | */ 199 | public function testSearchThrowExceptionWhenServerDown() { 200 | $client = \ElasticSearch\Client::connection(array( 201 | 'servers' => array( 202 | '127.0.0.1:9201' 203 | ) 204 | )); 205 | 206 | $this->assert->exception(function()use($client) { 207 | $client->search("title:cool"); 208 | })->isInstanceOf('ElasticSearch\\Transport\\HTTPException'); 209 | } 210 | 211 | /** 212 | * Test highlighting 213 | */ 214 | public function testHighlightedSearch() { 215 | $client = \ElasticSearch\Client::connection(); 216 | $ind = $client->index(array( 217 | 'title' => 'One cool document', 218 | 'body' => 'Lorem ipsum dolor sit amet', 219 | 'tag' => array('cool', "stuff", "2k") 220 | ), 1, array('refresh' => true)); 221 | $client->refresh(); 222 | 223 | $results = $client->search(array( 224 | 'query' => array( 225 | 'term' => array( 226 | 'title' => 'cool' 227 | ) 228 | ), 229 | 'highlight' => array( 230 | 'fields' => array( 231 | 'title' => new \stdClass() 232 | ) 233 | ) 234 | )); 235 | 236 | $this->assert->array($results)->hasKey('hits') 237 | ->array($results['hits'])->hasKey('hits') 238 | ->array($results['hits']['hits'])->isNotEmpty() 239 | ->array($results['hits']['hits'][0]) 240 | ->hasKey('highlight') 241 | ->array($results['hits']['hits'][0]['highlight']) 242 | ->hasKey('title'); 243 | } 244 | 245 | public function testConfigIsReadFromEnv() { 246 | $esURL = 'http://127.0.0.1:9200/index/type'; 247 | putenv("ELASTICSEARCH_URL={$esURL}"); 248 | 249 | $client = \ElasticSearch\Client::connection(); 250 | $config = $client->config(); 251 | $this->assert->array($config) 252 | ->hasKeys(array('index', 'type')); 253 | $this->assert->string($config['index'])->isEqualTo('index'); 254 | $this->assert->string($config['type'])->isEqualTo('type'); 255 | putenv("ELASTICSEARCH_URL"); 256 | } 257 | 258 | public function testBulk() { 259 | $esURL = 'http://127.0.0.1:9200/index/type'; 260 | putenv("ELASTICSEARCH_URL={$esURL}"); 261 | 262 | $client = \ElasticSearch\Client::connection(); 263 | $bulk = $client->beginBulk(); 264 | 265 | $doc = array( 266 | 'title' => 'First in Line' 267 | ); 268 | 269 | $client->index($doc, false, array('refresh' => true)); 270 | 271 | $doc2 = array( 272 | 'title' => 'Second in Line' 273 | ); 274 | $client->setType('type2'); 275 | $client->index($doc2, false); 276 | 277 | $client->setIndex('index2'); 278 | 279 | $client->delete(55); 280 | 281 | $operations = $bulk-> getOperations(); 282 | $this->assert->integer($bulk->count())->isEqualTo(3) 283 | ->array($operations[1]) 284 | ->hasSize(2) 285 | ->array($operations[0]) 286 | ->hasSize(2) 287 | ->array($operations[0][0])->hasKey('index') 288 | ->array($operations[0][0]['index'])->hasKey('_refresh') 289 | ->boolean($operations[0][0]['index']['_refresh'])->isEqualTo(true) 290 | ->array($operations[1][1])->isEqualTo($doc2) 291 | ->array($operations[2][0])->hasKey('delete') 292 | ->array($operations[2][0]['delete'])->hasKey('_id') 293 | ->integer($operations[2][0]['delete']['_id'])->isEqualTo(55) 294 | ; 295 | 296 | $payload = '{"index":{"_id":false,"_index":"index","_type":"type","_refresh":true}}' 297 | ."\n".'{"title":"First in Line"}' 298 | ."\n".'{"index":{"_id":false,"_index":"index","_type":"type2"}}' 299 | ."\n".'{"title":"Second in Line"}' 300 | ."\n".'{"delete":{"_id":55,"_index":"index2","_type":"type2"}}'."\n" 301 | ; 302 | 303 | $this->assert->string($bulk->createPayload())->isEqualTo($payload); 304 | 305 | // Run multiple bulks and make sure all documents are stored 306 | $client->beginBulk(); 307 | $client->index(array('title' => 'Bulk1'), 1); 308 | $client->index(array('title' => 'Bulk2'), 2); 309 | $client->commitBulk(); 310 | $client->beginBulk(); 311 | $client->index(array('title' => 'Bulk3'), 3); 312 | $client->index(array('title' => 'Bulk4'), 4); 313 | $client->commitBulk(); 314 | sleep(1); 315 | $resp = $client->search('title:Bulk*'); 316 | $this->assert->array($resp)->hasKey('hits') 317 | ->array($resp['hits'])->hasKey('total') 318 | ->integer($resp['hits']['total'])->isEqualTo(4); 319 | 320 | putenv("ELASTICSEARCH_URL"); 321 | } 322 | } 323 | --------------------------------------------------------------------------------