├── .gitignore ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── composer.json ├── docs ├── README.md └── _config.yml ├── query_builder_README.md ├── src ├── ArrayObject.php ├── AutoQueryBuilder.php ├── IModel.php ├── Model.php ├── MultiGetResult.php ├── Query.php ├── QueryBuilder.php ├── Result.php ├── SubQueryBuilder.php ├── TypeQuery.php ├── TypeQueryInterface.php └── TypeQueryTrait.php └── tests ├── BarModel.php ├── BarTypeQuery.php ├── FooModel.php ├── FooTypeQuery.php ├── QueryBuilder.php ├── TestsIndexQuery.php ├── ZeeModel.php ├── ZeeTypeQuery.php ├── case1.php ├── case2.php ├── case3.php └── case4.php /.gitignore: -------------------------------------------------------------------------------- 1 | composer.lock 2 | vendor 3 | vendor/ 4 | vendor/* 5 | nbproject 6 | nbproject/ 7 | nbproject/* 8 | /src/QueryBuilderTest.php -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at admin@itvision-sy.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 IT Vision Systems 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ElasticSearch PHP ORM and Query Builder (es-mapper) 2 | This is a simple ORM mapper for ElasticSearch for PHP. 3 | 4 | [ElasticSearch DSL query builder for PHP](./query_builder_README.md). 5 | 6 | ## Collaborators required 7 | If you can join me in updating and maintaining this project, please send a message to 8 | muhannad.shelleh@live.com 9 | 10 | ## Requirements 11 | - PHP 5.4+ 12 | - Elasticsearch PHP SDK v>=1 and <2 13 | - ElasticSearch server 1.6. ES2 is not tested, so use with care. 14 | 15 | ## Installation 16 | ### Composer 17 | ```composer require itvisionsy/php-es-orm``` 18 | 19 | ### Manual download 20 | Head to the latest version [here](https://github.com/itvisionsy/php-es-mapper/releases/latest) then download using one download button. 21 | 22 | ## How to use? 23 | 24 | **For the Query Builder, [read this README instead](./query_builder_README.md)** 25 | 26 | That is simple: 27 | 28 | ### Per index query: 29 | 1. Create a class extending the main query class (for general index use) . 30 | 1. Fill in the abstract methods. They are self-descriptive. 31 | 1. Use the created class `::find($type, $id)`, `::query($type, array $query =[])`, and `::all($type)` 32 | You will get a list of Model objects where you can object-property access to get all the info. 33 | i.e. `$model->name` to get the name property, ... 34 | 35 | ### Per type query 36 | 1. Create a class extending the type query class. 37 | OR create a class extending the main query class and implementing the `TypeQueryInterface` interface and use the `TypeQueryTrait` trait 38 | 1. Fill in the abstract methods. They are self-descriptive. 39 | 1. Use the methods: `::find($id)`, `::all()`, and `::query(array $query=[])`. 40 | You will get a list of Model objects the same way described above. 41 | 42 | #### Please note 43 | Methods' parameters are mapped to original elasticsearch methods and parameters as follows: 44 | * `::find(scalar)` and `::find(scalar[])` methods are mapped to [get](https://github.com/elastic/elasticsearch-php/blob/master/src/Elasticsearch/Client.php# L167) and [mget](https://github.com/elastic/elasticsearch-php/blob/master/src/Elasticsearch/Client.php# L671) methods respectively. 45 | * `::query` method is mapped to the [search](https://github.com/elastic/elasticsearch-php/blob/master/src/Elasticsearch/Client.php# L1002) method, and the $query param will be passed as is after appending the index and type parameters to it. 46 | 47 | ### Querying for data 48 | The query class is just a simple interface allows you to send DSL queries, or perform other ElasticSearch requests. 49 | The `::query()` method for example will expect to receive an assoc-array with a well-formed DSL query. 50 | 51 | However, you can use the query builder to builder the query and get a well-formed DSL array out of it. 52 | 53 | You can use a type-query query builder to build the query and execute it directly: 54 | ```PHP 55 | $result = TypeQuery::builder() 56 | ->where('key1','some value') 57 | ->where('key2',$intValue,'>') 58 | ->where('key3','value','!=') 59 | ->where('key4', ['value1','value2']) 60 | ->execute(); 61 | //$result is a \ItvisionSy\EsMapper\Result instance 62 | ``` 63 | 64 | Or you can use a generic query builder to build the query then you can modify it using other tools: 65 | ```PHP 66 | //init a builder object 67 | $builder = \ItvisionSy\EsMapper\QueryBuilder::make(); 68 | 69 | //build the query using different methods 70 | $query = $builder 71 | ->where('key1','some value') //term clause 72 | ->where('key2',$intValue,'>') //range clause 73 | ->where('key3','value','!=') //must_not term clause 74 | ->where('key4', ['value1','value2']) //terms clause 75 | ->where('email', '@hotmail.com', '*=') //wildcard search for all @hotmail.com emails 76 | ->sort('key1','asc') //first sort option 77 | ->sort('key2',['order'=>'asc','mode'=>'avg']) //second sort option 78 | ->from(20)->size(20) //results from 20 to 39 79 | ->toArray(); 80 | 81 | //modify the query as you need 82 | $query['aggs']=['company'=>['terms'=>['field'=>'company']]]; 83 | 84 | //then execute it against a type query 85 | $result = TypeQuery::query($query); 86 | //$result is a \ItvisionSy\EsMapper\Result instance 87 | ``` 88 | 89 | Please refer to [this file](./query_builder_README.md) for more detailed information. 90 | 91 | ## Retrieving results 92 | The returned result set implements the ArrayAccess interface to access specific document inside the result. i.e. 93 | ```PHP 94 | $result = SomeQuery::all(); 95 | ``` 96 | You can then get a document like this: 97 | ```PHP 98 | $doc = $result[1]; //gets the second document 99 | ``` 100 | Or you can use the dot notation like that: 101 | ``` 102 | $result->fetch('hits.hits.0'); //for any absolute access 103 | ``` 104 | 105 | ## Accessing document data 106 | On the model object, you can access the results in many ways: 107 | 1. using the object attribute accessor `$object->attribute` 108 | - if the attribute starts with underscore (_) then it will try to fetch it first from the meta information, then the attributes, and then from the internal object properties. 109 | - if the attribute starts with two underscores (__) then it will try to fetch first from the internal object properties, then attributes, then meta. 110 | - if not precedence underscores, then it will try to fetch from the attributes, then meta, then internal object properties. 111 | 1. using the `$object->getAttributes()[attribute]`, as the getAttributes() will return the document data as an array (first level only). 112 | 1. using the `$object->getAttributes($attribute1, $attribute2, ...)` which will return a single (or array) value[s] depending on the requested attributes 113 | 114 | ## Creating new documents 115 | Either way will work: 116 | 1. Use the index query static method 117 | ```php 118 | IndexQuery::create(array $data, $type, $id=null, array $parameters=[]) 119 | ``` 120 | 121 | 1. Use the type query static method: 122 | ```php 123 | TypeQuery::create(array $data, $id=null, array $parameters=[]) 124 | ``` 125 | 126 | ## Updating a document 127 | You can update an already indexed document by: 128 | 1. Either *Re-indexing* a document with the same type and id, OR 129 | 1. Or `update(array $data, array $parameters=[])` method on the model's object: 130 | 131 | ```php 132 | TypeQuery::find(1)->update(['new_key'=>'value','old_key'=>'new value'],[]); 133 | ``` 134 | 135 | ## Deleting a document 136 | The same way you can update a document, you can delete it: 137 | 1. Calling the static method `::delete($type, $id)` on the index query 138 | 1. Calling the method `->delete()` on model's object. 139 | 140 | ## Adding extra methods 141 | You may need to add extra custom methods like `top($numOfDocs)` or anything else. 142 | To do so, you need to create the method name you wish as protected in the query sub-class. The name should be prefixed with _ (i.e. `_top`) then, you can either 143 | * Call it prefixed with `get`, so to call the `_top(500)` method, just call `getTop(500)` and it will be mapped as public static and as public. 144 | * Override the `_allowPublicAccess` static protected method to add extra methods to expose. 145 | Please note that when overriding, don't forget to merge with the parent results not to lose the old ones: 146 | ```PHP 147 | protected _allowPublicAccess(){ 148 | return array_merge(parent::_allowPublicAccess(), ['top','any',...]); 149 | } 150 | ``` 151 | 152 | This way you will save the allowed methods from the parent. 153 | 154 | ### Extending the Model class 155 | You can extend the Model class easily. Just extend it! 156 | In case you were using the namespaces, you can set the models namespace in the query class by overriding the modelNamespace public method. This method should return a string ending with \ 157 | After that, you need to call the `->setModelClass($class)` on the query result object. 158 | 159 | ## Examples 160 | Please check [tests/](/tests) folder. Basically, the case1.php is the main file. 161 | 162 | ``` 163 | \ 164 | | 165 | +-- TestsIndexQuery (TestsIndexQuery.php) Main index query class. 166 | | | Maps results to \Models\ namespace. 167 | | | 168 | | +-- \FooTypeQuery (FooTypeQuery.php) Type index query class. 169 | | | 170 | | +-- \BarTypeQuery (BarTypeQuery.php) Type index query class. 171 | | 172 | |-- Models\ 173 | | 174 | +-- Foo (FooMode.php) Foo model class 175 | | 176 | +-- Bar (BarModel.php) Bar model class 177 | ``` 178 | 179 | ## License 180 | This code is published under [MIT](LICENSE) license. 181 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "itvisionsy/php-es-orm", 3 | "description": "An ElasticSearch PHP ORM and Query Builder", 4 | "replace": { 5 | "itvisionsy/es-orm": ">=1" 6 | }, 7 | "require": { 8 | "php": ">=5.4.0", 9 | "elasticsearch/elasticsearch": ">=1.0" 10 | }, 11 | "require-dev": { 12 | "phpunit/phpunit": "^5.0@dev" 13 | }, 14 | "license": "MIT", 15 | "authors": [ 16 | { 17 | "name": "Muhannad Shelleh", 18 | "email": "muhannad.shelleh@itvision-sy.com" 19 | } 20 | ], 21 | "minimum-stability": "dev", 22 | "autoload": { 23 | "psr-4": { 24 | "ItvisionSy\\EsMapper\\": "src" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # ElasticSearch PHP ORM and Query Builder (es-mapper) 2 | This is a simple ORM mapper for ElasticSearch for PHP. 3 | 4 | [ElasticSearch DSL query builder for PHP](./query_builder_README.md). 5 | 6 | ## Collaborators required 7 | If you can join me in updating and maintaining this project, please send a message to 8 | muhannad.shelleh@live.com 9 | 10 | ## Requirements 11 | - PHP 5.4+ 12 | - Elasticsearch PHP SDK v>=1 and <2 13 | - ElasticSearch server 1.6. ES2 is not tested, so use with care. 14 | 15 | ## Installation 16 | ### Composer 17 | ```composer require itvisionsy/php-es-orm``` 18 | 19 | ### Manual download 20 | Head to the latest version [here](https://github.com/itvisionsy/php-es-mapper/releases/latest) then download using one download button. 21 | 22 | ## How to use? 23 | 24 | **For the Query Builder, [read this README instead](./query_biulder_README.md)** 25 | 26 | That is simple: 27 | 28 | ### Per index query: 29 | 1. Create a class extending the main query class (for general index use) . 30 | 1. Fill in the abstract methods. They are self-descriptive. 31 | 1. Use the created class `::find($type, $id)`, `::query($type, array $query =[])`, and `::all($type)` 32 | You will get a list of Model objects where you can object-property access to get all the info. 33 | i.e. `$model->name` to get the name property, ... 34 | 35 | ### Per type query 36 | 1. Create a class extending the type query class. 37 | OR create a class extending the main query class and implementing the `TypeQueryInterface` interface and use the `TypeQueryTrait` trait 38 | 1. Fill in the abstract methods. They are self-descriptive. 39 | 1. Use the methods: `::find($id)`, `::all()`, and `::query(array $query=[])`. 40 | You will get a list of Model objects the same way described above. 41 | 42 | #### Please note 43 | Methods' parameters are mapped to original elasticsearch methods and parameters as follows: 44 | * `::find(scalar)` and `::find(scalar[])` methods are mapped to [get](https://github.com/elastic/elasticsearch-php/blob/master/src/Elasticsearch/Client.php# L167) and [mget](https://github.com/elastic/elasticsearch-php/blob/master/src/Elasticsearch/Client.php# L671) methods respectively. 45 | * `::query` method is mapped to the [search](https://github.com/elastic/elasticsearch-php/blob/master/src/Elasticsearch/Client.php# L1002) method, and the $query param will be passed as is after appending the index and type parameters to it. 46 | 47 | ### Querying for data 48 | The query class is just a simple interface allows you to send DSL queries, or perform other ElasticSearch requests. 49 | The `::query()` method for example will expect to receive an assoc-array with a well-formed DSL query. 50 | 51 | However, you can use the query builder to builder the query and get a well-formed DSL array out of it. 52 | 53 | You can use a type-query query builder to build the query and execute it directly: 54 | ```PHP 55 | $result = TypeQuery::builder() 56 | ->where('key1','some value') 57 | ->where('key2',$intValue,'>') 58 | ->where('key3','value','!=') 59 | ->where('key4', ['value1','value2']) 60 | ->execute(); 61 | //$result is a \ItvisionSy\EsMapper\Result instance 62 | ``` 63 | 64 | Or you can use a generic query builder to build the query then you can modify it using other tools: 65 | ```PHP 66 | //init a builder object 67 | $builder = \ItvisionSy\EsMapper\QueryBuilder::make(); 68 | 69 | //build the query using different methods 70 | $query = $builder 71 | ->where('key1','some value') //term clause 72 | ->where('key2',$intValue,'>') //range clause 73 | ->where('key3','value','!=') //must_not term clause 74 | ->where('key4', ['value1','value2']) //terms clause 75 | ->where('email', '@hotmail.com', '*=') //wildcard search for all @hotmail.com emails 76 | ->sort('key1','asc') //first sort option 77 | ->sort('key2',['order'=>'asc','mode'=>'avg']) //second sort option 78 | ->from(20)->size(20) //results from 20 to 39 79 | ->toArray(); 80 | 81 | //modify the query as you need 82 | $query['aggs']=['company'=>['terms'=>['field'=>'company']]]; 83 | 84 | //then execute it against a type query 85 | $result = TypeQuery::query($query); 86 | //$result is a \ItvisionSy\EsMapper\Result instance 87 | ``` 88 | 89 | Please refer to [this file](./query_builder_README.md) for more detailed information. 90 | 91 | ## Retrieving results 92 | The returned result set implements the ArrayAccess interface to access specific document inside the result. i.e. 93 | ```PHP 94 | $result = SomeQuery::all(); 95 | ``` 96 | You can then get a document like this: 97 | ```PHP 98 | $doc = $result[1]; //gets the second document 99 | ``` 100 | Or you can use the dot notation like that: 101 | ``` 102 | $result->fetch('hits.hits.0'); //for any absolute access 103 | ``` 104 | 105 | ## Accessing document data 106 | On the model object, you can access the results in many ways: 107 | 1. using the object attribute accessor `$object->attribute` 108 | - if the attribute starts with underscore (_) then it will try to fetch it first from the meta information, then the attributes, and then from the internal object properties. 109 | - if the attribute starts with two underscores (__) then it will try to fetch first from the internal object properties, then attributes, then meta. 110 | - if not precedence underscores, then it will try to fetch from the attributes, then meta, then internal object properties. 111 | 1. using the `$object->getAttributes()[attribute]`, as the getAttributes() will return the document data as an array (first level only). 112 | 1. using the `$object->getAttributes($attribute1, $attribute2, ...)` which will return a single (or array) value[s] depending on the requested attributes 113 | 114 | ## Creating new documents 115 | Either way will work: 116 | 1. Use the index query static method 117 | ```php 118 | IndexQuery::create(array $data, $type, $id=null, array $parameters=[]) 119 | ``` 120 | 121 | 1. Use the type query static method: 122 | ```php 123 | TypeQuery::create(array $data, $id=null, array $parameters=[]) 124 | ``` 125 | 126 | ## Updating a document 127 | You can update an already indexed document by: 128 | 1. Either *Re-indexing* a document with the same type and id, OR 129 | 1. Or `update(array $data, array $parameters=[])` method on the model's object: 130 | 131 | ```php 132 | TypeQuery::find(1)->update(['new_key'=>'value','old_key'=>'new value'],[]); 133 | ``` 134 | 135 | ## Deleting a document 136 | The same way you can update a document, you can delete it: 137 | 1. Calling the static method `::delete($type, $id)` on the index query 138 | 1. Calling the method `->delete()` on model's object. 139 | 140 | ## Adding extra methods 141 | You may need to add extra custom methods like `top($numOfDocs)` or anything else. 142 | To do so, you need to create the method name you wish as protected in the query sub-class. The name should be prefixed with _ (i.e. `_top`) then, you can either 143 | * Call it prefixed with `get`, so to call the `_top(500)` method, just call `getTop(500)` and it will be mapped as public static and as public. 144 | * Override the `_allowPublicAccess` static protected method to add extra methods to expose. 145 | Please note that when overriding, don't forget to merge with the parent results not to lose the old ones: 146 | ```PHP 147 | protected _allowPublicAccess(){ 148 | return array_merge(parent::_allowPublicAccess(), ['top','any',...]); 149 | } 150 | ``` 151 | 152 | This way you will save the allowed methods from the parent. 153 | 154 | ### Extending the Model class 155 | You can extend the Model class easily. Just extend it! 156 | In case you were using the namespaces, you can set the models namespace in the query class by overriding the modelNamespace public method. This method should return a string ending with \ 157 | After that, you need to call the `->setModelClass($class)` on the query result object. 158 | 159 | ## Examples 160 | Please check [tests/](/tests) folder. Basically, the case1.php is the main file. 161 | 162 | ``` 163 | \ 164 | | 165 | +-- TestsIndexQuery (TestsIndexQuery.php) Main index query class. 166 | | | Maps results to \Models\ namespace. 167 | | | 168 | | +-- \FooTypeQuery (FooTypeQuery.php) Type index query class. 169 | | | 170 | | +-- \BarTypeQuery (BarTypeQuery.php) Type index query class. 171 | | 172 | |-- Models\ 173 | | 174 | +-- Foo (FooMode.php) Foo model class 175 | | 176 | +-- Bar (BarModel.php) Bar model class 177 | ``` 178 | 179 | ## License 180 | This code is published under [MIT](LICENSE) license. 181 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /query_builder_README.md: -------------------------------------------------------------------------------- 1 | # ElasticSearch DSL Query Builder Class 2 | This is a simple Query Builder for ElasticSearch DSL language 3 | 4 | ## Installation 5 | It comes with the [php-es-mapper package](http://github.com/itvisionsy/php-es-mapper). 6 | Read about it [here](./README.md). 7 | 8 | ## How to use 9 | There are two ways to instantiate a builder object. 10 | 11 | ### Generic query builder 12 | The query builder is not related to a specific TypeQuery class, it helps to build the query array and retrieve it. 13 | The built array will be then explicitly sent to some query class for execution. 14 | ```PHP 15 | //init a builder object 16 | $builder = \ItvisionSy\EsMapper\QueryBuilder::make(); 17 | 18 | //build the query using different methods 19 | $query = $builder 20 | ->where('key1','some value') //term clause 21 | ->where('key2',$intValue,'>') //range clause 22 | ->where('key3','value','!=') //must_not term clause 23 | ->where('key4', ['value1','value2']) //terms clause 24 | ->where('email', '@hotmail.com', '*=') //wildcard search for all @hotmail.com emails 25 | ->sort('key1','asc') //first sort option 26 | ->sort('key2',['order'=>'asc','mode'=>'avg']) //second sort option 27 | ->from(20)->size(20) //results from 20 to 39 28 | ->toArray(); 29 | 30 | //modify the query as you need 31 | $query['aggs']=['company'=>['terms'=>['field'=>'company']]]; 32 | 33 | //then execute it against a type query 34 | $result = TypeQuery::query($query); 35 | //$result is a \ItvisionSy\EsMapper\Result instance 36 | ``` 37 | 38 | ### Auto implicit query builder 39 | This type is implicitly created using a `TypeQuery::builder()` method. 40 | 41 | It acts like the generic query builder, but in addition to everything the generic query builder can do, 42 | it allows you to execute the result query directly from within the query builder instant itself. 43 | 44 | ```PHP 45 | $result = TypeQuery::builder() 46 | ->where('key1','some value') //term clause 47 | ->where('key2',$intValue,'>') //range clause 48 | ->where('key3','value','!=') //must_not term clause 49 | ->where('key4', ['value1','value2']) //terms clause 50 | ->where('email', '@hotmail.com', '*=') //wildcard search for all @hotmail.com emails 51 | ->sort('key1','asc') //first sort option 52 | ->sort('key2',['order'=>'asc','mode'=>'avg']) //second sort option 53 | ->from(20)->size(20) //results from 20 to 39 54 | ->execute(); 55 | //$result is a \ItvisionSy\EsMapper\Result instance 56 | ``` 57 | 58 | ## Building a query 59 | 60 | ### The available ElasticSearch methods 61 | The query builder allows the main filter/query clauses: `term`, `match`, `wildcard`, `regexp`, `range`, and `prefix`. 62 | Each of these methods can be used against a key, or multiple keys, or a value, or multiple values, giving 63 | flexible way to build up a really complex queries. 64 | 65 | ### The smart `where` method 66 | The `::where($key, $value, $compare, $filter)` method is also provided, which will scan, detect, and convert 67 | itself to one of the previous available methods. You can use the $compare to tell it what you are looking for, 68 | and the $filter to enforce a filter or query clause. 69 | 70 | Following are the different compare values: 71 | 72 | Compare | Result | Sample | Sample means 73 | --- | --- | --- | --- 74 | default, =, term, equals | [term query] | ``` $builder->where('name','Muhannad') ``` | name = "Muhannad" 75 | \>, gt | [range query] | ``` $builder->where('age',15,'>') ``` | age > 15 76 | \>=, gte | [range query] | ``` $builder->where('age',15,'>=') ``` | age >= 15 77 | \<, lt | [range query] | ``` $builder->where('age',50,'<') ``` | age < 50 78 | \<=, lte | [range query] | ``` $builder->where('age',50,'<=') ``` | age <= 50 79 | \><, <>, between | [range query] | ``` $builder->where('age',[15,50],'<>') ``` | 15 < age < 50 80 | \>=<=, <=>=, between from to | [range query] | ``` $builder->where('age',[15,50],'<=>=') ``` | 15 <= age <= 50 81 | \>=<, <>=, between from | [range query] | ``` $builder->where('age',[15,50],'<>=') ``` | 15 <= age < 50 82 | \><=, <=>, between to | [range query] | ``` $builder->where('age',[15,50],'<=>') ``` | 15 < age <= 50 83 | *=, suffix, suffixed, ends with, ends | [wildcard query] | ``` $builder->where('name','nad', '*=') ``` | name = "*nad" 84 | \=\*, prefix, prefixed, starts with, starts | [prefix query] | ``` $builder->where('name','Muh', '=*') ``` | name = "Muh*" 85 | \*=\*, like, wildcard | [wildcard query] | ``` $builder->where('name','*anna*', 'like') ``` | name = "\*anna\*" 86 | \*\*, r, regexp, regex, rx | [regexp query] | ``` $builder->where('wealth', '[1-9]\d{6}', 'regex') ``` | wealth is 7 digits number 87 | *, match | [match query] | ``` $builder->where('address', 'Dubai UAE', 'match') ``` | address has words 'Dubai' 'UAE' 88 | 89 | [term query]: https://www.elastic.co/guide/en/elasticsearch/reference/1.6/query-dsl-term-query.html 90 | [range query]: https://www.elastic.co/guide/en/elasticsearch/reference/1.6/query-dsl-range-query.html 91 | [wildcard query]: https://www.elastic.co/guide/en/elasticsearch/reference/1.6/query-dsl-wildcard-query.html 92 | [prefix query]: https://www.elastic.co/guide/en/elasticsearch/reference/1.6/query-dsl-prefix-query.html 93 | [regexp query]: https://www.elastic.co/guide/en/elasticsearch/reference/1.6/query-dsl-regexp-query.html 94 | [match query]: https://www.elastic.co/guide/en/elasticsearch/reference/1.6/query-dsl-match-query.html 95 | 96 | ### Sorting 97 | The `::sort($key, $order)` allows you to add multiple sort levels. 98 | 99 | The `$order` parameter can be a simple 'desc' or 'asc' value, or can be an assoc array as per ElasticSearch [sort documentation](https://www.elastic.co/guide/en/elasticsearch/reference/1.6/search-request-sort.html). 100 | 101 | The third `$override` parameter allows you to clear the sort section before adding the sort terms. 102 | 103 | 104 | ### Pagination 105 | You can use `from`, and/or `size` for detailed control, or use the `page($size, $from)` which will make two different calls to each method alone. 106 | 107 | ### Extra methods 108 | If you need to add extra clauses, there are several methods which allow you to: 109 | `raw`, `rawMustFilter`, `rawMustQuery`, `rawMustNotFilter`, `rawMustNotQuery`, `rawShouldFilter`, and `rawShouldQuery`. 110 | 111 | The raw query allows you to add any query/filter clause as an assoc-array to one of the filter/query [bool](https://www.elastic.co/guide/en/elasticsearch/reference/1.6/query-dsl-bool-query.html) clause sections. 112 | 113 | ### Sub queries 114 | 115 | Sub queries will be automatically created when needed. However, you can add subqueries using the `andWhere` and/or `orWhere` methods. 116 | 117 | Each of the two methods will start a new subquery, and returns a new query builder object of class `\ItvisionSy\EsMapper\SubQueryBuilder` where you can add extra filter clauses as you want. 118 | 119 | When you are done with the subquery, you can finish it and return to the main query by calling the `->endSubQuery()` method. 120 | 121 | For now, the subqueries can only be added to the filter bool clause sections. 122 | 123 | ## Finish the query 124 | 125 | ### Get the final query array/JSON 126 | Once done, call the `->toArray()` or `->toJSON()` to get the final query array or JSON string. 127 | 128 | ### Empty the query 129 | You can also empty the query and start a new query. This is handy if you have a main base query and you want to execute it multiple time with some minor changes. 130 | ```PHP 131 | $baseQuery = [ 132 | 'query'=>[ 133 | 'filtered'=>[ 134 | 'filter'=>[ 135 | 'bool'=>[ 136 | //some complex base query 137 | ] 138 | ] 139 | ] 140 | ] 141 | ]; 142 | $builder = CustomersTypeQuery::builder($baseQuery); 143 | $page1 = $builder->page(10,0)->execute(); 144 | $page2 = $builder->emptyQuery($baseQuery)->page(10,10)->execute(); 145 | ``` 146 | This is just a sample to show you how the empty query works. You can come up with different use cases and scenarios. 147 | 148 | ### Execute the query 149 | If the main query builder is instantiated from within a `TypeQueryInterface` compliant query class, then you can execute the query directly using the `->execute()` method. 150 | 151 | This method is going to call the instanciator TypeQuery `query` method passing the final query array as a parameter. 152 | 153 | ## Sample 154 | ```PHP 155 | $result = CustomersQuery::builder() //all customers 156 | ->where('email','@hotmail.com', '*=') //who have hotmail.com emails 157 | ->where(['created','updated'],['today','yesterday']) //and were active today or yesterday 158 | ->orWhere() //starts a subquery 159 | ->where('country',['UAE','KSA','TUR']) //where country is UAE, KSA, or TUR 160 | ->where('age', [10,20], '<=>=') //or the age is between 10 and 20 years 161 | ->endSubQuery() //ends the sub query to the main query 162 | ->rawMustNotFilter([ //add a raw nested must-not filter 163 | 'nested'=>[ 164 | 'path'=>'visits', 165 | 'filter'=>[ 166 | 'term'=>['ip'=>'192.168.0.5'] 167 | ] 168 | ] 169 | ]) //where no visit from a specific ip 170 | ->sort('updated','desc') //most recent first 171 | ->page(10, 20) //10 results starting from 20 172 | ->execute(); 173 | ``` 174 | This query will result in the following DSL query: 175 | ```JSON 176 | { 177 | "query":{ 178 | "filtered":{ 179 | "query":{ 180 | "bool":{ 181 | "must":[ 182 | { 183 | "wildcard":{ 184 | "email":{ 185 | "wildcard":"*@hotmail.com" 186 | } 187 | } 188 | } 189 | ] 190 | } 191 | }, 192 | "filter":{ 193 | "bool":{ 194 | "must":[ 195 | { 196 | "or":[ 197 | { 198 | "terms":{ 199 | "created":{ 200 | "value":[ 201 | "today", 202 | "yesterday" 203 | ] 204 | } 205 | } 206 | }, 207 | { 208 | "terms":{ 209 | "updated":{ 210 | "value":[ 211 | "today", 212 | "yesterday" 213 | ] 214 | } 215 | } 216 | } 217 | ] 218 | }, 219 | { 220 | "or":[ 221 | { 222 | "terms":{ 223 | "country":{ 224 | "value":[ 225 | "UAE", 226 | "KSA", 227 | "TUR" 228 | ] 229 | } 230 | } 231 | }, 232 | { 233 | "range":{ 234 | "age":{ 235 | "gte":10, 236 | "lte":20 237 | } 238 | } 239 | } 240 | ] 241 | } 242 | ], 243 | "must_not":[ 244 | { 245 | "nested":{ 246 | "path":"visits", 247 | "filter":{ 248 | "term":{ 249 | "ip":"192.168.0.5" 250 | } 251 | } 252 | } 253 | } 254 | ] 255 | } 256 | } 257 | } 258 | }, 259 | "sort":[ 260 | { 261 | "":"desc" 262 | } 263 | ], 264 | "from":20, 265 | "size":10 266 | } 267 | ``` 268 | 269 | ## License 270 | This code is published under [MIT](LICENSE) license. 271 | -------------------------------------------------------------------------------- /src/ArrayObject.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class ArrayObject extends PHPArrayObject implements IModel { 26 | 27 | public static function make(array $array) { 28 | return new static($array); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/AutoQueryBuilder.php: -------------------------------------------------------------------------------- 1 | 24 | */ 25 | class AutoQueryBuilder extends QueryBuilder { 26 | 27 | protected $queryInstance; 28 | 29 | /** 30 | * Sets the query instance to be used 31 | * 32 | * @param Query $instance 33 | * @return AutoQueryBuilder 34 | */ 35 | public function setQueryInstance(Query $instance) { 36 | $this->queryInstance = $instance; 37 | return $this; 38 | } 39 | 40 | /** 41 | * Executes the query against the passed query instance 42 | * 43 | * @return Result 44 | * @throws BadMethodCallException 45 | */ 46 | public function execute() { 47 | if (!$this->queryInstance) { 48 | throw new BadMethodCallException("Query instance is not passed. Consider calling \$object->setQueryInstance(\$istance) first"); 49 | } 50 | return $this->queryInstance->query($this->toArray()); 51 | } 52 | 53 | /** 54 | * Initiates a new query builder for a specific Query instance 55 | * 56 | * This helps in fluiding the query stream. i.e. 57 | * SomeTypeQuery::make($config)->builder()->where("id",8)->where("age",">=",19)->execute(); 58 | * 59 | * 60 | * @param Query $instance 61 | * @param array $query 62 | * @return AutoQueryBuilder 63 | */ 64 | public static function makeForQueryInstance(Query $instance, array $query = []) { 65 | $query = static::make($query); 66 | return $query->setQueryInstance($instance); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /src/IModel.php: -------------------------------------------------------------------------------- 1 | 25 | */ 26 | interface IModel extends ArrayAccess, Iterator { 27 | 28 | public static function make(array $array); 29 | } 30 | -------------------------------------------------------------------------------- /src/Model.php: -------------------------------------------------------------------------------- 1 | 25 | * 26 | */ 27 | class Model implements IModel { 28 | 29 | /** 30 | * The raw hit element array returned from ES 31 | * @var array 32 | */ 33 | protected $esHitData; 34 | 35 | /** 36 | * The es client object 37 | * @var Client 38 | */ 39 | protected $esClient; 40 | 41 | /** 42 | * Meta information about the model 43 | * @var array contains the following (not always): 44 | * @var mixed id The id of the document. 45 | * @var string type The type of the document. 46 | * @var string index The index of the document. 47 | * @var float score The score of the hit. 48 | */ 49 | protected $meta = []; 50 | 51 | /** 52 | * The data array from the raw 53 | * @var array 54 | */ 55 | protected $attributes = []; 56 | 57 | /** 58 | * A custom factory method that will try to detect the correct model class 59 | * and instantiate and object of it. 60 | * 61 | * @param array $esHit 62 | * @param string $className a class name or pattern. 63 | * @param Client $esClient ElasticSearch client 64 | * @return Model 65 | */ 66 | public static function MakeOfType(array $esHit, $className = "", Client $esClient = null) { 67 | if ($className && strpos($className, '{type}') !== false) { 68 | $baseClassName = str_replace(' ', '', ucwords(preg_replace('/[^a-zA-Z0-9]+/', ' ', $esHit['_type']))); 69 | $fullClassName = str_replace("{type}", $baseClassName, $className); 70 | if (!class_exists($fullClassName)) { 71 | $fullClassName = array_key_exists('_source', $esHit) ? static::class : ArrayObject::class; 72 | } 73 | } else { 74 | $fullClassName = $className? : static::class; 75 | } 76 | return $fullClassName::make($esHit, $esClient); 77 | } 78 | 79 | /** 80 | * A factory method to create a new class object for the provided hit data. 81 | * 82 | * @param array $esHit 83 | * @param Client $esClient ElasticSearch client 84 | * @return Model 85 | */ 86 | public static function make(array $esHit, Client $esClient = null) { 87 | return new static($esHit, $esClient); 88 | } 89 | 90 | /** 91 | * Constructor method 92 | * 93 | * @param array $esHit 94 | * @param Client $esClient ElasticSearch client 95 | */ 96 | public function __construct(array $esHit, Client $esClient = null) { 97 | $this->esHitData = $esHit; 98 | $this->esClient = $esClient; 99 | $source = array_key_exists("_source", $esHit) ? "_source" : "fields"; 100 | $this->attributes = $esHit[$source]; 101 | unset($esHit[$source]); 102 | foreach ($esHit as $key => $value) { 103 | $key = trim($key, "_"); 104 | $this->meta[$key] = $value; 105 | } 106 | } 107 | 108 | public function __get($name) { 109 | if (substr($name, 0, 2) == '__') { 110 | $tName = substr($name, 1); 111 | return property_exists($this, $tName) ? $this->$tName : ($this->offsetExists($name) ? $this->attributes[$name] : (array_key_exists($tName, $this->meta) ? $this->meta[$tName] : null)); 112 | } elseif (substr($name, 0, 1) == '_') { 113 | $tName = substr($name, 1); 114 | return array_key_exists($tName, $this->meta) ? $this->meta[$tName] : ($this->offsetExists($name) ? $this->attributes[$name] : (property_exists($this, $tName) ? $this->$tName : null)); 115 | } else { 116 | return $this->offsetExists($name) ? $this->attributes[$name] : (array_key_exists($name, $this->meta) ? $this->meta[$name] : (property_exists($this, $name) ? $this->$name : null)); 117 | } 118 | } 119 | 120 | public function __set($name, $value) { 121 | $this->offsetSet($name, $value); 122 | } 123 | 124 | /** 125 | * Returns true if there is an attribute named $offset, false otherwise 126 | * 127 | * @param string $offset 128 | * @return boolean 129 | */ 130 | public function offsetExists($offset) { 131 | return array_key_exists($offset, $this->attributes); 132 | } 133 | 134 | /** 135 | * Returns the attribute specified in $offset. 136 | * 137 | * @param string $offset 138 | * @return mixed 139 | */ 140 | public function offsetGet($offset) { 141 | return @$this->attributes[$offset]; 142 | } 143 | 144 | /** 145 | * Sets the attribute name of the $offset 146 | * 147 | * @param string $offset 148 | * @param mixed $value 149 | * @return Model 150 | */ 151 | public function offsetSet($offset, $value) { 152 | $this->attributes[$offset] = $value; 153 | return $this; 154 | } 155 | 156 | public function canAlter() { 157 | if (!array_key_exists('id', $this->meta)) { 158 | return false; 159 | } 160 | if (!array_key_exists('type', $this->meta)) { 161 | return false; 162 | } 163 | if (!array_key_exists('index', $this->meta)) { 164 | return false; 165 | } 166 | return true; 167 | } 168 | 169 | /** 170 | * Sets elasticsearch client 171 | * @param Client $esClient 172 | */ 173 | public function setElasticClient(Client $esClient) { 174 | $this->esClient = $esClient; 175 | } 176 | 177 | /** 178 | * Returns elastic hit data (the original data provided). 179 | * 180 | * @return array 181 | */ 182 | public function getEsHitData() { 183 | return $this->esHitData; 184 | } 185 | 186 | /** 187 | * Return attributes (actual document data) 188 | * @return array 189 | */ 190 | public function getAttributes() { 191 | $values = func_get_args(); 192 | if (count($values) === 1) { 193 | $result = []; 194 | foreach ($values as $value) { 195 | $result[$value] = @$this->attributes[$value]; 196 | } 197 | return $result; 198 | } elseif (count($values) > 1) { 199 | return @$this->attributes[$values[0]]; 200 | } else { 201 | return $this->attributes; 202 | } 203 | } 204 | 205 | /** 206 | * Return meta data 207 | * @return array 208 | */ 209 | public function getMeta($key = null) { 210 | if ($key) { 211 | return $this->meta[$key]; 212 | } 213 | return $this->meta; 214 | } 215 | 216 | /** 217 | * ElasticSearch Client 218 | * @return Client|null 219 | */ 220 | public function getElasticClient() { 221 | return $this->esClient; 222 | } 223 | 224 | /** 225 | * Updates the document 226 | * 227 | * @param array $data the normal elastic update parameters to be used 228 | * @param array $parameters the parameters array 229 | * @return array 230 | * @throws Exception 231 | */ 232 | public function update(array $data, array $parameters = []) { 233 | if (!$this->canAlter()) { 234 | throw new Exception('Need index, type, and key to update a document'); 235 | } 236 | if (!$this->esClient) { 237 | throw new Exception('Need ElasticSearch client object for ALTER operations'); 238 | } 239 | $params = [ 240 | 'index' => $this->meta['index'], 241 | 'type' => $this->meta['type'], 242 | 'id' => $this->meta['id'], 243 | 'body' => $data] + $parameters; 244 | return $this->esClient->update($params); 245 | } 246 | 247 | /** 248 | * Deletes a document 249 | * 250 | * @param array $parameters 251 | * @return array 252 | * @throws Exception 253 | */ 254 | public function delete(array $parameters = []) { 255 | if (!$this->canAlter()) { 256 | throw new Exception('Need index, type, and key to delete a document'); 257 | } 258 | if (!$this->esClient) { 259 | throw new Exception('Need ElasticSearch client object for ALTER operations'); 260 | } 261 | $params = [ 262 | 'index' => $this->meta['index'], 263 | 'type' => $this->meta['type'], 264 | 'id' => $this->meta['id']] + $parameters; 265 | $result = $this->esClient->delete($params); 266 | return $result; 267 | } 268 | 269 | /** 270 | * 271 | * @param type $offset 272 | * @return Model 273 | */ 274 | public function offsetUnset($offset) { 275 | unset($this->attributes[$offset]); 276 | return $this; 277 | } 278 | 279 | public function current() { 280 | return current($this->attributes); 281 | } 282 | 283 | public function key() { 284 | return key($this->attributes); 285 | } 286 | 287 | public function next() { 288 | return next($this->attributes); 289 | } 290 | 291 | public function rewind() { 292 | return rewind($this->attributes); 293 | } 294 | 295 | public function valid() { 296 | return valid($this->attributes); 297 | } 298 | 299 | } 300 | -------------------------------------------------------------------------------- /src/MultiGetResult.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | class MultiGetResult extends Result { 25 | 26 | /** 27 | * Returns the number of the hits 28 | * 29 | * @return integer 30 | */ 31 | public function count() { 32 | return count($this->result['docs']); 33 | } 34 | 35 | /** 36 | * Returns the highest score in the hists 37 | * 38 | * @return float 39 | */ 40 | public function score() { 41 | return 1; 42 | } 43 | 44 | /** 45 | * Returns the raw data array in the hist. 46 | * This is the raw returned hits list from ES. 47 | * 48 | * @return array 49 | */ 50 | public function data() { 51 | return $this->result; 52 | } 53 | 54 | /** 55 | * Returns the current array index or document object at the offset. 56 | * 57 | * @see Result::useIndexKeys() 58 | * @see Result::useDocumentKeys() 59 | * @return mixed 60 | */ 61 | public function key() { 62 | if ($this->indexKeys) { 63 | return $this->currentIndex; 64 | } else { 65 | return $this->result['docs'][$this->currentIndex]['_id']; 66 | } 67 | } 68 | 69 | /** 70 | * Returns true if the $offset exsits in the results array, false otherwise. 71 | * IT uses the array index keys for the offset. 72 | * 73 | * @param integer $offset 74 | * @return boolean 75 | */ 76 | public function offsetExists($offset) { 77 | return array_key_exists($offset, $this->result['docs']); 78 | } 79 | 80 | /** 81 | * Gets the document object in the $offset 82 | * 83 | * @param integer $offset 84 | * @return Model 85 | */ 86 | public function offsetGet($offset) { 87 | return Model::makeOfType($this->result['docs'][$offset], $this->modelClass); 88 | } 89 | 90 | } 91 | -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | 29 | * 30 | * @method Result query(string $type, array $parameters=[]) 31 | * @method-static Result query(string $type, array $parameters=[]) 32 | * @method AutoQueryBuilder builder() 33 | * @method-static AutoQueryBuilder builder() 34 | * @method Result all(string $type) 35 | * @method-static Result all(string $type) 36 | * @method Result find(string $type, int|string|int[]|string $id) 37 | * @method-static Result find(string $type, int|string|int[]|string $id) 38 | * @method IModel create(array $data, string $index, int|string $id, array $parameters=[]) 39 | * @method-static IModel create(array $data, string $index, int|string $id, array $parameters=[]) 40 | * @method array delete(string|string $index, string|string[] $type, int|string $id, array $parameters=[]) 41 | * @method-static array delete(string|string $index, string|string[] $type, int|string $id, array $parameters=[]) 42 | * @property-read Client $client 43 | */ 44 | abstract class Query { 45 | 46 | protected $config; 47 | protected $client; 48 | 49 | /** 50 | * Factory method for creating query objects 51 | * 52 | * @param array $config The config to use for the client 53 | * @return Query 54 | */ 55 | public static function make(array $config = []) { 56 | return new static($config); 57 | } 58 | 59 | /** 60 | * Query object constructor 61 | * 62 | * @param array $config 63 | */ 64 | public function __construct(array $config = []) { 65 | $config += $this->defaults(); 66 | $this->config = $config; 67 | $this->establish($config, true); 68 | } 69 | 70 | /** 71 | * The index name to be used. 72 | * Should return a string ends with namespace separator path or 73 | * contains the {type} string as a placeholder for the actual model class name. 74 | * 75 | * i.e. 76 | * 77 | * return "\\"; 78 | * //means the model classes for Foo and Bar will be \Foo and \Bar 79 | * 80 | * return "\\Models\\"; 81 | * //means the model classes for Foo and Bar will be \Models\Foo and 82 | * // \Models\Bar 83 | * 84 | * return "\\Models\\{type}Model"; 85 | * //means the model classes for Foo and Bar will be \Models\FooModel and 86 | * // \Models\BarModel 87 | * 88 | * @return string 89 | */ 90 | abstract public function index(); 91 | 92 | /** 93 | * Gets the namespace/class pattern will be used to create Model objects. 94 | * Should end with \\ 95 | * Default value is \ means the root global namespace. 96 | * 97 | * @return string 98 | */ 99 | public function modelClassNamePattern() { 100 | return "\\"; 101 | } 102 | 103 | /** 104 | * Gets the index name for this query class 105 | * @return string 106 | */ 107 | public function getIndex() { 108 | return $this->index(); 109 | } 110 | 111 | /** 112 | * Gets the current ES client in use for the object. 113 | * 114 | * @return Client 115 | */ 116 | public function getClient() { 117 | return $this->client; 118 | } 119 | 120 | /** 121 | * Establishes the ES client using the provided config. 122 | * Provided config values will take place over the object's config attribute. 123 | * Finally, the defaults() result will be used as a final config source. 124 | * 125 | * @param array $config Config value to override the general config params 126 | * @return Client 127 | */ 128 | public function establish(array $config = [], $assign = false) { 129 | $client = new Client($config + $this->config + $this->defaults()); 130 | if ($assign) { 131 | $this->client = $client; 132 | } 133 | return $client; 134 | } 135 | 136 | /** 137 | * A decorator class to get all index type documents. 138 | * 139 | * @param string $type 140 | * @return Result|Model[] 141 | */ 142 | protected function _all($type) { 143 | return $this->__query($this->index(), $type); 144 | } 145 | 146 | /** 147 | * A decorator method to search for ES documents. 148 | * 149 | * @param string $type 150 | * @param array $query 151 | * @return Result|Model[] 152 | */ 153 | protected function _query($type, array $query = []) { 154 | return $this->__query($this->index(), $type, $query); 155 | } 156 | 157 | /** 158 | * A decorator method to get specific document by its id. 159 | * If the id is and array of strings or integers, 160 | * then multiple documents will be retreived by id. 161 | * 162 | * @param string $type 163 | * @param mixed|string|integer|mixed[]|int[]|string[] $id 164 | * @return Model 165 | */ 166 | protected function _find($type, $id) { 167 | if (is_array($id)) { 168 | return $this->__mfind($this->index(), $type, $id); 169 | } else { 170 | return $this->__find($this->index(), $type, $id); 171 | } 172 | } 173 | 174 | /** 175 | * Index a new document 176 | * 177 | * @param array $data the document data 178 | * @param string $type 179 | * @param string|number $id 180 | * @param array $parameters 181 | * @return array 182 | */ 183 | protected function _create(array $data, $type, $id = null, array $parameters = []) { 184 | return $this->__create($data, $this->index(), $type, $id, $parameters); 185 | } 186 | 187 | /** 188 | * Index a new document 189 | * 190 | * @param array $data the document data 191 | * @param string $index 192 | * @param string $type 193 | * @param string|number $id 194 | * @param array $parameters 195 | * @return array 196 | */ 197 | protected function __create(array $data, $index, $type, $id = null, array $parameters = []) { 198 | $parameters+=["api" => "create"]; 199 | $api = strtolower($parameters["api"]) == "index" ? "index" : "create"; 200 | unset($parameters["api"]); 201 | $result = $this->client->$api([ 202 | 'index' => $index, 203 | 'type' => $type, 204 | 'body' => $data] + ($id ? ['id' => $id] : []) + $parameters); 205 | if (array_key_exists('_shards', $result) && array_key_exists('failed', $result['_shards']) && $result['_shards']['failed'] > 0) { 206 | throw new Exception('Failed to create the document. Serialized results: ' + json_encode($result)); 207 | } else { 208 | $result = $this->__find($result['_index'], $result['_type'], $result['_id']); 209 | } 210 | return $result; 211 | } 212 | 213 | /** 214 | * Deletes a document 215 | * 216 | * @param string $type 217 | * @param string|number $id 218 | * @param array $parameters 219 | * @return array 220 | */ 221 | protected function _delete($type, $id = null, array $parameters = []) { 222 | return $this->__delete($this->index(), $type, $id, $parameters); 223 | } 224 | 225 | /** 226 | * Deletes a document 227 | * 228 | * @param string $index 229 | * @param string $type 230 | * @param string|number $id 231 | * @param array $parameters 232 | * @return array 233 | */ 234 | protected function __delete($index, $type, $id, array $parameters = []) { 235 | $result = $this->client->delete([ 236 | 'index' => $index, 237 | 'type' => $type, 238 | 'id' => $id] + $parameters); 239 | return $result; 240 | } 241 | 242 | /** 243 | * Add fixed query part to the called query 244 | * 245 | * To add a fixed per-class condition. I.e. specific flag to be set to 246 | * specific value. 247 | * 248 | * Note that this is to be called for the query (not find) calls only. 249 | * 250 | * @return array The query part to be merged into the original part. 251 | */ 252 | protected function additionalQuery() { 253 | return []; 254 | } 255 | 256 | /** 257 | * The actual method to call client's search method. 258 | * Returns Result object 259 | * 260 | * @param string $index 261 | * @param string $type if null, then all types will be searched 262 | * @param array $query 263 | * @return Result|Model[] 264 | */ 265 | protected function __query($index, $type = null, array $query = []) { 266 | $result = $this->client->search([ 267 | 'index' => $index, 268 | 'body' => array_merge_recursive($query, $this->additionalQuery()) 269 | ] + ($type ? ['type' => $type] : [])); 270 | return $this->_makeResult($result); 271 | } 272 | 273 | /** 274 | * The actual method to call client's get method. 275 | * Returns either a Model object or null on failure. 276 | * 277 | * @param string $index 278 | * @param string $type 279 | * @param sring|int $id 280 | * @return null|Model 281 | */ 282 | protected function __find($index, $type, $id) { 283 | try { 284 | $result = $this->client->get([ 285 | 'index' => $index, 286 | 'type' => $type, 287 | 'id' => $id 288 | ]); 289 | if ($result['found']) { 290 | return $this->_makeModel($result); 291 | } else { 292 | return null; 293 | } 294 | } catch (\Exception $e) { 295 | trigger_error($e->getMessage(), E_USER_WARNING); 296 | return null; 297 | } 298 | } 299 | 300 | /** 301 | * The actual method to call client's mget method. 302 | * Returns either a result of Model objects or null on failure. 303 | * 304 | * @param string $index 305 | * @param string $type 306 | * @param sring[]|int[] $ids 307 | * @return null|Model 308 | */ 309 | protected function __mfind($index, $type, $ids) { 310 | try { 311 | $docs = $this->client->mget([ 312 | 'index' => $index, 313 | 'type' => $type, 314 | 'body' => [ 315 | "ids" => $ids 316 | ] 317 | ]); 318 | $result = ['ids' => $ids, 'found' => [], 'missed' => [], 'docs' => []]; 319 | $missed = [] + $ids; 320 | foreach ($docs['docs'] as $doc) { 321 | if ($doc['found']) { 322 | $result['docs'][] = $doc; 323 | $result['found'][] = $doc['_id']; 324 | unset($missed[array_search($doc['_id'], $missed)]); 325 | } 326 | } 327 | $result['missed'] = $missed; 328 | return $this->_makeMultiGetResult($result); 329 | } catch (\Exception $e) { 330 | trigger_error($e->getMessage(), E_USER_WARNING); 331 | return null; 332 | } 333 | } 334 | 335 | /** 336 | * Creates a results set for ES query hits 337 | * 338 | * @param array $result 339 | * @return Result 340 | */ 341 | protected function _makeResult(array $result) { 342 | return Result::make($result, $this->getClient())->setModelClass($this->_fullModelClassNamePattern()); 343 | } 344 | 345 | /** 346 | * Creates a results set for ES query hits 347 | * 348 | * @param array $result 349 | * @return Result 350 | */ 351 | protected function _makeMultiGetResult(array $result) { 352 | return MultiGetResult::make($result, $this->getClient())->setModelClass($this->_fullModelClassNamePattern()); 353 | } 354 | 355 | /** 356 | * Creates a model object for a specific es result hits entry 357 | * 358 | * @param array $source 359 | * @return Model 360 | */ 361 | protected function _makeModel(array $source) { 362 | return Model::MakeOfType($source, $this->_fullModelClassNamePattern(), $this->getClient()); 363 | } 364 | 365 | /** 366 | * Returns the full namespace string for the model. 367 | * If the provided namespace contains {type} then use it, 368 | * otherwize add {type} to the end. 369 | * 370 | * @return string 371 | */ 372 | protected function _fullModelClassNamePattern() { 373 | return stripos($this->modelClassNamePattern()? : "", '{type}') !== false ? $this->modelClassNamePattern() : "{$this->modelClassNamePattern()}{type}"; 374 | } 375 | 376 | public static function __callStatic($name, $arguments) { 377 | 378 | if (array_search($name, static::_allowPublicAccess()) !== false) { 379 | //pass specific methods 380 | return call_user_func_array([new static(), $name], $arguments); 381 | } 382 | 383 | if (strpos($name, 'get', 0) === 0) { 384 | //pass getAll() as an internal query 385 | return call_user_func_array([new static(), $name], $arguments); 386 | } 387 | 388 | trigger_error("Call to undefined static method " . static::class . "::{$name}", E_USER_ERROR); 389 | } 390 | 391 | public function __call($name, $arguments) { 392 | if (array_search($name, static::_allowPublicAccess()) !== false) { 393 | //pass specific methods 394 | return call_user_func_array([$this, "_{$name}"], $arguments); 395 | } 396 | if (strpos($name, 'get', 0) === 0) { 397 | //pass getAll() as an internal query 398 | $methodName = lcfirst(substr($name, 3)); 399 | return call_user_func_array([$this, "_{$methodName}"], $arguments); 400 | } 401 | trigger_error("Call to undefined method " . static::class . "::{$name}", E_USER_ERROR); 402 | } 403 | 404 | /** 405 | * The default values for the client 406 | * 407 | * @return array 408 | */ 409 | protected function defaults() { 410 | return [ 411 | 'hosts' => [ 412 | 'http://localhost:9200/' 413 | ] 414 | ]; 415 | } 416 | 417 | /** 418 | * Used to expose extra methods to the public static or public calls 419 | * 420 | * Should return an array of strings. 421 | * i.e. 422 | * return ['any','top']; 423 | * Will allow public static and public access to the two new methods: 424 | * protected _any() and protected _top($rows) 425 | * 426 | * Note that the protected methods should be prefixed with _ 427 | * 428 | * When overriding in sub classes use this form: 429 | * protected _allowPublicAccess(){ 430 | * return array_merge(parent::_allowPublicAccess(), ['method',...,...]); 431 | * } 432 | * This way you will save the allowed methods from the parent. 433 | * 434 | * @return array 435 | */ 436 | protected static function _allowPublicAccess() { 437 | return [ 438 | 'all', 439 | 'query', 440 | 'find', 441 | 'meta', 442 | 'create', 443 | 'delete', 444 | 'builder', 445 | 'metaSettings', 446 | 'metaAliases', 447 | 'metaMappings', 448 | 'metaWarmers' 449 | ]; 450 | } 451 | 452 | /** 453 | * Retreives the meta data of an index 454 | * @param string|array $features a list of meta objects to fetch. null means 455 | * everything. Can be 456 | * * 1 string (i.e. '_settings'), 457 | * * csv (i.e. '_settings,_aliases'), 458 | * * array (i.e. ['_settings','_aliases'] 459 | * @param array $options can contain: 460 | * ['ignore_unavailable'] 461 | * (bool) Whether specified concrete indices should be ignored 462 | * when unavailable (missing or closed) 463 | * ['allow_no_indices'] 464 | * (bool) Whether to ignore if a wildcard indices expression 465 | * resolves into no concrete indices. (This includes `_all` 466 | * string or when no indices have been specified) 467 | * ['expand_wildcards'] 468 | * (enum) Whether to expand wildcard expression to concrete 469 | * indices that are open, closed or both. 470 | * ['local'] 471 | * (bool) Return local information, do not retrieve the state 472 | * from master node (default: false) 473 | * @return array 474 | */ 475 | protected function _meta($features = null, array $options = []) { 476 | if ($features) { 477 | $features = join(',', array_map(function($item) { 478 | return '_' . strtolower(trim($item, '_')); 479 | }, is_scalar($features) ? explode(",", $features) : $features)); 480 | } 481 | $options = ['index' => $this->index()] + $options + ($features ? ['feature' => $features] : []); 482 | $result = $this->client->indices()->get($options); 483 | $result = array_pop($result); 484 | return $result; 485 | } 486 | 487 | /** 488 | * Retreives just the settings of the index 489 | * @param array $options check _meta() for details 490 | * @return array 491 | */ 492 | protected function _metaSettings(array $options = []) { 493 | $result = $this->_meta('_settings', $options); 494 | return array_pop($result); 495 | } 496 | 497 | /** 498 | * Retreives just the aliases of the index 499 | * @param array $options check _meta() for details 500 | * @return array 501 | */ 502 | protected function _metaAliases(array $options = []) { 503 | $result = $this->_meta('_aliases', $options); 504 | return array_pop($result); 505 | } 506 | 507 | /** 508 | * Retreives just the mappings of the index 509 | * @param array $options check _meta() for details 510 | * @return array 511 | */ 512 | protected function _metaMappings(array $options = []) { 513 | $result = $this->_meta('_mappings', $options); 514 | return array_pop($result); 515 | } 516 | 517 | /** 518 | * Retreives just the warmers of the index 519 | * @param array $options check _meta() for details 520 | * @return array 521 | */ 522 | protected function _metaWarmers(array $options = []) { 523 | $result = $this->_meta('_warmers', $options); 524 | return array_pop($result); 525 | } 526 | 527 | /** 528 | * Creates a builder for this query 529 | * 530 | * @param array $query A query array to start using it 531 | * @return AutoQueryBuilder 532 | */ 533 | protected function _builder(array $query = []) { 534 | return $this->__builder($query); 535 | } 536 | 537 | /** 538 | * Creates a builder for this query 539 | * 540 | * @param array $query A query array to start using it 541 | * @return AutoQueryBuilder 542 | */ 543 | protected function __builder(array $query = []) { 544 | return AutoQueryBuilder::makeForQueryInstance($this, $query); 545 | } 546 | 547 | } 548 | -------------------------------------------------------------------------------- /src/QueryBuilder.php: -------------------------------------------------------------------------------- 1 | where('name','Elastic') //term:{name:'Elastic'} 28 | * ->where('version', ['1.6','2.0']) //terms:{version:['1.6','2.0']} 29 | * ->where(['sdk','client'],'PHP') //or:[{term:{sdk:'PHP'}},{term:{...}}] 30 | * ->where(['created','updated'],['yesterday','today']) //or:[{terms},{terms}] 31 | * ->toArray(); //get the final query array 32 | * 33 | * It worths to know that the query builder depends on the bool query/filter: 34 | * [ 35 | * 'query'=>[ 36 | * 'filtered'=>[ 37 | * 'filter'=>[ 38 | * 'bool'=>[ 39 | * 'must'=>[...], 40 | * 'should'=>[...], 41 | * 'must_not'=>[...], 42 | * ] 43 | * ], 44 | * 'query'=>[ 45 | * 'bool'=>[ 46 | * 'must'=>[...], 47 | * 'should'=>[...], 48 | * 'must_not'=>[...], 49 | * ] 50 | * ] 51 | * ] 52 | * ] 53 | * ] 54 | * 55 | * @package ItvisionSy\EsMapper 56 | * @author Muhannad Shelleh 57 | * 58 | * @method static|self|QueryBuilder make(array $baseQuery=[]) 59 | * @method-static static|self|QueryBuilder make(array $baseQuery=[]) 60 | * 61 | * @see AutoQueryBuilder 62 | * 63 | */ 64 | class QueryBuilder { 65 | 66 | protected $query = []; 67 | 68 | public static function __callStatic($name, $arguments) { 69 | switch ($name) { 70 | case 'make': 71 | return static::__make(array_key_exists(0, $arguments) ? $arguments[0] : []); 72 | } 73 | } 74 | 75 | public function __call($name, $arguments) { 76 | $queryName = "{$name}Query"; 77 | if (method_exists($this, $queryName)) { 78 | return call_user_func_array([$this, $queryName], $arguments); 79 | } 80 | } 81 | 82 | protected static function __make(array $query = []) { 83 | return new static($query); 84 | } 85 | 86 | /** 87 | * 88 | * @param array $query The base query to start building on it 89 | */ 90 | public function __construct(array $query = []) { 91 | $this->query = array_merge_recursive($query, []); 92 | } 93 | 94 | /** 95 | * Returns the final DSL query as a PHP assoc array 96 | * @return array 97 | */ 98 | public function toArray() { 99 | return $this->query; 100 | } 101 | 102 | /** 103 | * Returns the final DSL query as a JSON string 104 | * @return type 105 | */ 106 | public function toJSON() { 107 | return json_encode($this->query); 108 | } 109 | 110 | /** 111 | * Resets the query to an empty query. 112 | * 113 | * @return QueryBuilder|static|self 114 | */ 115 | public function emptyQuery(array $baseQuery = []) { 116 | $this->query = $baseQuery; 117 | return $this; 118 | } 119 | 120 | /** 121 | * Adds a sort clause to the query 122 | * 123 | * @param string $by the key to sort by it 124 | * @param string|array $direction the direction of the sort. 125 | * Can be an array for extra sort control. 126 | * @param boolean $override false by default. if true it will reset the sort 127 | * first, then add the new sort entry. 128 | * @return QueryBuilder|static|self 129 | */ 130 | public function sort($by, $direction = "asc", $override = false) { 131 | $this->assertInitiated("sort"); 132 | if ($override) { 133 | $this->query['sort'] = []; 134 | } 135 | $this->query['sort'][] = [$by => $direction]; 136 | return $this; 137 | } 138 | 139 | /** 140 | * The basic smart method to build queries. 141 | * 142 | * It will automatically identify the correct filter/query tool to use, as 143 | * well the correct query part to be used in, depending on the content and 144 | * type of the key, value, and comparison parameters. 145 | * 146 | * $comparison parameter will define what filter/query tool to use. 147 | * 148 | * @param string|string[] $key the key(s) to check 149 | * @param string|string[] $value the value(s) to check against 150 | * @param string $compare the comparison operator or elastic query/filter tool 151 | * prefix it with ! to negate or with ? to convert to should 152 | * @param boolean $filter false to add as a query, true as a filter 153 | * @param array|map $params additional parameters the query/filter can use 154 | * @return static|QueryBuilder 155 | */ 156 | public function where($key, $value, $compare = "=", $filter = null, $params = []) { 157 | 158 | //identify the bool part must|should|must_not 159 | $compare = trim($compare); 160 | $bool = substr($compare, 0, 1) == "!" ? "must_not" : (substr($compare, 0, 1) == "?" ? "should" : "must"); 161 | 162 | //get the correct compare value 163 | if ($bool !== "must") { 164 | $compare = substr($compare, 1); 165 | } 166 | 167 | //$suffix and $prefix will be used in regex, wildcard, prefix, and suffix 168 | //queries. 169 | $tool = $suffix = $prefix = null; 170 | //$_filter is the real identifier for the filter 171 | $_filter = $filter; 172 | 173 | //identify the tool, operator, and part 174 | switch (strtolower(str_replace("_", " ", $compare))) { 175 | case '=': 176 | case 'equals': 177 | case 'term': 178 | default: 179 | $tool = "term"; 180 | break; 181 | case '>': 182 | case 'gt': 183 | $tool = "range"; 184 | $operator = "gt"; 185 | break; 186 | case '<': 187 | case 'lt': 188 | $tool = "range"; 189 | $operator = "lt"; 190 | break; 191 | case '><': 192 | case '<>': 193 | case 'between': 194 | $tool = "range"; 195 | $operator = ["gt", "lt"]; 196 | break; 197 | case '>=<=': 198 | case '<=>=': 199 | case 'between from to': 200 | $tool = "range"; 201 | $operator = ["gte", "lte"]; 202 | break; 203 | case '>=<': 204 | case '<>=': 205 | case 'between from': 206 | $tool = "range"; 207 | $operator = ["gte", "lt"]; 208 | break; 209 | case '><=': 210 | case '<=>': 211 | case 'between to': 212 | $tool = "range"; 213 | $operator = ["gt", "lte"]; 214 | break; 215 | case '>=': 216 | case 'gte': 217 | $tool = "range"; 218 | $operator = "gte"; 219 | break; 220 | case '<=': 221 | case 'lte': 222 | $tool = "range"; 223 | $operator = "lte"; 224 | break; 225 | case '*=': 226 | case 'suffix': 227 | case 'suffixed': 228 | case 'ends with': 229 | case 'ends': 230 | $tool = "wildcard"; 231 | $prefix = "*"; 232 | $_filter = false; 233 | break; 234 | case '=*': 235 | case 'starts': 236 | case 'starts with': 237 | case 'prefix': 238 | case 'prefixed': 239 | $tool = "prefix"; 240 | break; 241 | case '*=*': 242 | case 'like': 243 | case 'wildcard': 244 | $tool = "wildcard"; 245 | $prefix = '*'; 246 | $suffix = '*'; 247 | $_filter = false; 248 | break; 249 | case '**': 250 | case 'r': 251 | case 'regexp': 252 | case 'regex': 253 | case 'rx': 254 | $tool = "regexp"; 255 | break; 256 | case '*': 257 | case 'match': 258 | $tool = "match"; 259 | $_filter = false; 260 | break; 261 | } 262 | 263 | //add prefix/suffix to each element in array values 264 | if ($suffix || $prefix) { 265 | if (is_array($value)) { 266 | foreach ($value as $index => $singleValue) { 267 | if (is_string($singleValue)) { 268 | $value[$index] = $prefix . $singleValue . $suffix; 269 | } 270 | } 271 | } else { 272 | $value = ($prefix? : "") . $value . ($suffix? : ""); 273 | } 274 | } 275 | 276 | //call the real query builder method 277 | switch ($tool) { 278 | case 'match': 279 | return $this->match($key, $value, $bool, $this->shouldBeFilter($filter, $_filter), $params); 280 | case 'regexp': 281 | return $this->regexp($key, $value, $bool, $this->shouldBeFilter($filter, $_filter), $params); 282 | case 'term': 283 | return $this->term($key, $value, $bool, $this->shouldBeFilter($filter, $_filter), $params); 284 | case 'range': 285 | return $this->range($key, $operator, $value, $bool, $this->shouldBeFilter($filter, $_filter), $params); 286 | case 'wildcard': 287 | return $this->wildcard($key, $value, $bool, $this->shouldBeFilter($filter, $_filter), $params); 288 | case 'prefix': 289 | return $this->prefix($key, $value, $bool, $this->shouldBeFilter($filter, $_filter), $params); 290 | } 291 | } 292 | 293 | /** 294 | * Creates a wildcard query part. 295 | * 296 | * It will automatically detect the $key and $value types to create the 297 | * required number of clauses, as follows: 298 | * 299 | * $key $value result 300 | * 301 | * single single {wildcard:{$key:{wildcard:$value}}} 302 | * single array or:[wildcard($key, $value[1]), ...] 303 | * array single or:[wildcard($key[1], $value), ...] 304 | * array array or:[wildcard($key[1], $value[1]), 305 | * wildcard($key[1], $value[2]), ... 306 | * wildcard($key[2], $value[1]), ... ] 307 | * 308 | * If $filter is true, it will enclose the wildcard query with `query` 309 | * filter and add it to the DSL query filter section instead the query 310 | * section. 311 | * 312 | * @param string|string[] $key the key(s) to wildcard search in. 313 | * @param string|string[] $value the value(s) to wildcard search against. 314 | * @param string $bool severity of the query/filter. [must]|must_not|should 315 | * @param bool $filter if true, DSL query filter section will be used after 316 | * enclosing in a `query` filter. 317 | * @param array $params extra parameters for the query tool 318 | * @return QueryBuilder|static|self 319 | */ 320 | public function wildcard($key, $value, $bool = "must", $filter = false, array $params = []) { 321 | if (is_array($key)) { 322 | return $this->multiWildcard($key, $value, $bool, $filter, $params); 323 | } 324 | if (is_array($value)) { 325 | return $this->wildcards($key, $value, $bool, $filter, $params); 326 | } 327 | $this->addBool($this->makeFilteredQuery(["wildcard" => [$key => ["wildcard" => $value]]], $filter), $bool, $filter, $params); 328 | return $this; 329 | } 330 | 331 | /** 332 | * Creates wildcard query for each $value grouped by OR clause 333 | * 334 | * @see QueryBuilder::wildcard() for more information 335 | * 336 | * @param string|string[] $key the key(s) to wildcard search in. 337 | * @param string[] $values the value(s) to wildcard search against. 338 | * @param string $bool severity of the query/filter. [must]|must_not|should 339 | * @param bool $filter if true, DSL query filter section will be used after 340 | * enclosing in a `query` filter. 341 | * @param array $params extra parameters for the query tool 342 | * @return QueryBuilder|static|self 343 | */ 344 | public function wildcards($key, array $values, $bool = "must", $filter = false, array $params = []) { 345 | $subQuery = $this->orWhere($bool); 346 | foreach ($values as $value) { 347 | $subQuery->wildcard($key, $value, $bool, true, $params); 348 | } 349 | $subQuery->endSubQuery(); 350 | return $this; 351 | } 352 | 353 | /** 354 | * Creates wildcard query for each $key grouped by OR clause 355 | * 356 | * @see QueryBuilder::wildcard() for more information 357 | * 358 | * @param string[] $keys the key(s) to wildcard search in. 359 | * @param string|string[] $value the value(s) to wildcard search against. 360 | * @param string $bool severity of the query/filter. [must]|must_not|should 361 | * @param bool $filter if true, DSL query filter section will be used after 362 | * enclosing in a `query` filter. 363 | * @param array $params extra parameters for the query tool 364 | * @return QueryBuilder|static|self 365 | */ 366 | public function multiWildcard(array $keys, $value, $bool = "must", $filter = false, array $params = []) { 367 | $subQuery = $this->orWhere($bool); 368 | foreach ($keys as $key) { 369 | $subQuery->wildcard($key, $value, $bool, true, $params); 370 | } 371 | $subQuery->endSubQuery(); 372 | return $this; 373 | } 374 | 375 | /** 376 | * Creates wildcard query for each $key/$value pair grouped by OR clause 377 | * 378 | * @see QueryBuilder::wildcard() for more information 379 | * 380 | * @param string[] $keys the key(s) to wildcard search in. 381 | * @param string[] $values the value(s) to wildcard search against. 382 | * @param string $bool severity of the query/filter. [must]|must_not|should 383 | * @param bool $filter if true, DSL query filter section will be used after 384 | * enclosing in a `query` filter. 385 | * @param array $params extra parameters for the query tool 386 | * @return QueryBuilder|static|self 387 | */ 388 | public function multiWildcards(array $keys, array $values, $bool = "must", $filter = false, array $params = []) { 389 | $subQuery = $this->orWhere($bool); 390 | foreach ($keys as $key) { 391 | $subQuery->wildcards($key, $values, $bool, true, $params); 392 | } 393 | $subQuery->endSubQuery(); 394 | return $this; 395 | } 396 | 397 | /** 398 | * Creates a regexp query part. 399 | * 400 | * It will automatically detect the $key and $value types to create the 401 | * required number of clauses, as follows: 402 | * 403 | * $key $value result 404 | * 405 | * single single {regexp:{$key:{regexp:$value}}} 406 | * single array or:[regexp($key, $value[1]), ...] 407 | * array single or:[regexp($key[1], $value), ...] 408 | * array array or:[regexp($key[1], $value[1]), 409 | * regexp($key[1], $value[2]), ... 410 | * regexp($key[2], $value[1]), ... ] 411 | * 412 | * If $filter is true, it will enclose the regexp query with `query` 413 | * filter and add it to the DSL query filter section instead the query 414 | * section. 415 | * 416 | * @param string|string[] $key the key(s) to regexp search in. 417 | * @param string|string[] $value the value(s) to regexp search against. 418 | * @param string $bool severity of the query/filter. [must]|must_not|should 419 | * @param bool $filter if true, DSL query filter section will be used after 420 | * enclosing in a `query` filter. 421 | * @param array $params extra parameters for the query tool 422 | * @return QueryBuilder|static|self 423 | */ 424 | public function regexp($key, $value, $bool = "must", $filter = false, array $params = []) { 425 | if (is_array($key)) { 426 | return $this->multiRegexp($key, $value, $bool, $filter, $params); 427 | } 428 | if (is_array($value)) { 429 | return $this->regexps($key, $value, $bool, $filter, $params); 430 | } 431 | $this->addBool($this->makeFilteredQuery(["regexp" => [$key => ["value" => $value]]], $filter), $bool, $filter, $params); 432 | return $this; 433 | } 434 | 435 | /** 436 | * Creates regexp query for each $value grouped by OR clause 437 | * 438 | * @see QueryBuilder::regexp() for more information 439 | * 440 | * @param string|string[] $key the key(s) to regexp search in. 441 | * @param string[] $values the value(s) to regexp search against. 442 | * @param string $bool severity of the query/filter. [must]|must_not|should 443 | * @param bool $filter if true, DSL query filter section will be used after 444 | * enclosing in a `query` filter. 445 | * @param array $params extra parameters for the query tool 446 | * @return QueryBuilder|static|self 447 | */ 448 | public function regexps($key, array $values, $bool = "must", $filter = false, array $params = []) { 449 | $subQuery = $this->orWhere($bool); 450 | foreach ($values as $value) { 451 | $subQuery->regexp($key, $value, $bool, true, $params); 452 | } 453 | $subQuery->endSubQuery(); 454 | return $this; 455 | } 456 | 457 | /** 458 | * Creates regexp query for each $key grouped by OR clause 459 | * 460 | * @see QueryBuilder::regexp() for more information 461 | * 462 | * @param string[] $keys the key(s) to regexp search in. 463 | * @param string|string[] $value the value(s) to regexp search against. 464 | * @param string $bool severity of the query/filter. [must]|must_not|should 465 | * @param bool $filter if true, DSL query filter section will be used after 466 | * enclosing in a `query` filter. 467 | * @param array $params extra parameters for the query tool 468 | * @return QueryBuilder|static|self 469 | */ 470 | public function multiRegexp(array $keys, $value, $bool = "must", $filter = false, array $params = []) { 471 | $subQuery = $this->orWhere($bool); 472 | foreach ($keys as $key) { 473 | $subQuery->regexp($key, $value, $bool, true, $params); 474 | } 475 | $subQuery->endSubQuery(); 476 | return $this; 477 | } 478 | 479 | /** 480 | * Creates regexp query for each $key/$value pair grouped by OR clause 481 | * 482 | * @see QueryBuilder::regexp() for more information 483 | * 484 | * @param string[] $keys the key(s) to regexp search in. 485 | * @param string[] $values the value(s) to regexp search against. 486 | * @param string $bool severity of the query/filter. [must]|must_not|should 487 | * @param bool $filter if true, DSL query filter section will be used after 488 | * enclosing in a `query` filter. 489 | * @param array $params extra parameters for the query tool 490 | * @return QueryBuilder|static|self 491 | */ 492 | public function multiRegexps(array $keys, array $values, $bool = "must", $filter = false, array $params = []) { 493 | $subQuery = $this->orWhere($bool); 494 | foreach ($keys as $key) { 495 | $subQuery->regexps($key, $values, $bool, true, $params); 496 | } 497 | $subQuery->endSubQuery(); 498 | return $this; 499 | } 500 | 501 | /** 502 | * Creates a prefix query part. 503 | * 504 | * It will automatically detect the $key and $value types to create the 505 | * required number of clauses, as follows: 506 | * 507 | * $key $value result 508 | * 509 | * single single {prefix:{$key:{prefix:$value}}} 510 | * single array or:[prefix($key, $value[1]), ...] 511 | * array single or:[prefix($key[1], $value), ...] 512 | * array array or:[prefix($key[1], $value[1]), 513 | * prefix($key[1], $value[2]), ... 514 | * prefix($key[2], $value[1]), ... ] 515 | * 516 | * If $filter is true, it will enclose the prefix query with `query` 517 | * filter and add it to the DSL query filter section instead the query 518 | * section. 519 | * 520 | * @param string|string[] $key the key(s) to prefix search in. 521 | * @param string|string[] $value the value(s) to prefix search against. 522 | * @param string $bool severity of the query/filter. [must]|must_not|should 523 | * @param bool $filter if true, DSL query filter section will be used after 524 | * enclosing in a `query` filter. 525 | * @param array $params extra parameters for the query tool 526 | * @return QueryBuilder|static|self 527 | */ 528 | public function prefix($key, $value, $bool = "must", $filter = false, array $params = []) { 529 | if (is_array($key)) { 530 | return $this->multiPrefix($key, $value, $bool, $filter, $params); 531 | } 532 | if (is_array($value)) { 533 | return $this->prefixs($key, $value, $bool, $filter, $params); 534 | } 535 | $this->addBool($this->makeFilteredQuery(["prefix" => [$key => $value]], $filter), $bool, $filter, $params); 536 | return $this; 537 | } 538 | 539 | /** 540 | * Creates prefix query for each $value grouped by OR clause 541 | * 542 | * @see QueryBuilder::prefix() for more information 543 | * 544 | * @param string|string[] $key the key(s) to prefix search in. 545 | * @param string[] $values the value(s) to prefix search against. 546 | * @param string $bool severity of the query/filter. [must]|must_not|should 547 | * @param bool $filter if true, DSL query filter section will be used after 548 | * enclosing in a `query` filter. 549 | * @param array $params extra parameters for the query tool 550 | * @return QueryBuilder|static|self 551 | */ 552 | public function prefixs($key, array $values, $bool = "must", $filter = false, array $params = []) { 553 | $subQuery = $this->orWhere($bool); 554 | foreach ($values as $value) { 555 | $subQuery->prefix($key, $value, $bool, true, $params); 556 | } 557 | $subQuery->endSubQuery(); 558 | return $this; 559 | } 560 | 561 | /** 562 | * Creates OR-joined multiple prefix queries for the list of keys 563 | * 564 | * Creates a prefix query for the $value for each key in the $keys 565 | * 566 | * @param array $keys 567 | * @param type $value 568 | * @param type $bool 569 | * @param array $params 570 | * @return QueryBuilder|static|self 571 | */ 572 | public function multiPrefix(array $keys, $value, $bool = "must", $filter = false, array $params = []) { 573 | $subQuery = $this->orWhere($bool); 574 | foreach ($keys as $key) { 575 | $subQuery->prefix($key, $value, $bool, true, $params); 576 | } 577 | $subQuery->endSubQuery(); 578 | return $this; 579 | } 580 | 581 | /** 582 | * Creates multiple prefixes queries for the list of keys 583 | * 584 | * Two keys and two values will results in 4 prefix queries 585 | * 586 | * @param array $keys 587 | * @param array $values 588 | * @param type $bool 589 | * @param array $params 590 | * @return QueryBuilder|static|self 591 | */ 592 | public function multiPrefixs(array $keys, array $values, $bool = "must", $filter = false, array $params = []) { 593 | $subQuery = $this->orWhere($bool); 594 | foreach ($keys as $key) { 595 | $subQuery->prefixs($key, $values, $bool, true, $params); 596 | } 597 | $subQuery->endSubQuery(); 598 | return $this; 599 | } 600 | 601 | /** 602 | * A normal term/terms query 603 | * 604 | * It will automatically use term or terms query depending on the type of 605 | * $value parameter whether it is an array or not. 606 | * 607 | * @param type $key 608 | * @param type $value 609 | * @param type $bool 610 | * @param type $filter 611 | * @param array $params 612 | * @return QueryBuilder|static|self 613 | */ 614 | public function term($key, $value, $bool = "must", $filter = false, array $params = []) { 615 | if (is_array($key)) { 616 | return $this->multiTerm($key, $value, $bool, $filter, $params); 617 | } 618 | $tool = "term" . (is_array($value) ? "s" : ""); 619 | $this->addBool([$tool => [$key => (is_array($value) ? $value : ["value" => $value])]], $bool, $filter, $params); 620 | return $this; 621 | } 622 | 623 | /** 624 | * Multiple OR joined term queries 625 | * 626 | * @param array $keys 627 | * @param type $value 628 | * @param type $bool 629 | * @param type $filter 630 | * @param array $params 631 | * @return QueryBuilder|static|self 632 | */ 633 | public function multiTerm(array $keys, $value, $bool = "must", $filter = false, array $params = []) { 634 | $subQuery = $this->orWhere($bool); 635 | foreach ($keys as $key) { 636 | $subQuery->term($key, $value, $bool, true, $params); 637 | } 638 | $subQuery->endSubQuery(); 639 | return $this; 640 | } 641 | 642 | /** 643 | * A match query 644 | * 645 | * @param type $key 646 | * @param type $value 647 | * @param type $bool 648 | * @param boolean $filter 649 | * @param array $params 650 | * @return QueryBuilder|static|self 651 | */ 652 | public function match($key, $value, $bool, $filter = false, array $params = []) { 653 | if (is_array($key)) { 654 | return $this->multiMatch($key, $value, $bool, $filter, $params); 655 | } 656 | if (is_array($value)) { 657 | return $this->matches($key, $value, $bool, $filter, $params); 658 | } 659 | $this->addBool($this->makeFilteredQuery(["match" => [$key => ["query" => $value] + $params]], $filter), $bool, $filter); 660 | return $this; 661 | } 662 | 663 | /** 664 | * Creates multiple match queries for each value in the array 665 | * 666 | * Queries will be joined by an OR filter 667 | * 668 | * @param string $key the key to create the matches for 669 | * @param array $values a list of values to create a match query for each 670 | * @param type $bool 671 | * @param array $params 672 | * @return QueryBuilder|static|self 673 | */ 674 | public function matches($key, array $values, $bool, $filter = false, array $params = []) { 675 | $subQuery = $this->orWhere($bool); 676 | foreach ($values as $value) { 677 | $subQuery->match($key, $value, $bool, true, $params); 678 | } 679 | $subQuery->endSubQuery(); 680 | return $this; 681 | } 682 | 683 | /** 684 | * Creates a mutli_match query 685 | * 686 | * @param array $keys keys(fields) of the multimatch 687 | * @param type $value 688 | * @param type $bool 689 | * @param array $params 690 | * @return QueryBuilder|static|self 691 | */ 692 | public function multiMatch(array $keys, $value, $bool, $filter = false, array $params = []) { 693 | $this->addBool($this->makeFilteredQuery(["multi_match" => ["query" => $value, "fields" => $keys] + $params], $filter), $bool, $filter); 694 | return $this; 695 | } 696 | 697 | /** 698 | * Creates multiple mutli_match queries for each value in the array 699 | * 700 | * Queries will be joined by an OR filter 701 | * 702 | * @param array $keys keys(fields) of the multimatch 703 | * @param scalar[] $values a list of values to create a multimatch query for 704 | * @param type $bool 705 | * @param array $params 706 | * @return QueryBuilder|static|self 707 | */ 708 | public function multiMatches(array $keys, array $values, $bool, $filter = false, array $params = []) { 709 | $subQuery = $this->orWhere($bool); 710 | foreach ($values as $value) { 711 | $subQuery->multiMatch($keys, $value, $bool, true, $params); 712 | } 713 | $subQuery->endSubQuery(); 714 | return $this; 715 | } 716 | 717 | /** 718 | * Creates a range query 719 | * 720 | * @param string $key The key to create the range query for 721 | * @param string|string[] $operator lt, gt, lte, gte. Can be an array of mixed lt,gt,lte,gte; and if so, it should match the number of elements in the $value array too. 722 | * @param scalar|scalar[] $value the value for the range comparison. Should be an array of same element count if the $operator is also an array. 723 | * @param string $bool must, should, or must_not 724 | * @param boolean $filter to use the filter part instead of the query part 725 | * @param array $params additional query parameters for the range query 726 | * @return QueryBuilder|static|self 727 | * @throws \BadMethodCallException 728 | */ 729 | public function range($key, $operator, $value, $bool, $filter, array $params = []) { 730 | if (is_array($operator) && !is_array($value) || !is_array($operator) && is_array($value) || is_array($operator) && count($operator) !== count($value)) { 731 | throw new \BadMethodCallException("Operator and value parameters should be both a scalar type or both an array with same number of elements"); 732 | } 733 | if (is_array($key)) { 734 | return $this->multiRange($key, $operator, $value, $bool, $filter, $params); 735 | } 736 | $query = []; 737 | $operators = (array) $operator; 738 | $values = (array) $value; 739 | foreach ($operators as $index => $operator) { 740 | $query[$operator] = $values[$index]; 741 | } 742 | $this->addBool(["range" => [$key => $query]], $bool, $filter, $params); 743 | return $this; 744 | } 745 | 746 | /** 747 | * Creates multiple range queries joined by OR 748 | * 749 | * For each key in the keys, a new range query will be created 750 | * 751 | * @param array $keys keys to create a range query for each of them 752 | * @param string|string[] $operator lt, gt, lte, gte. Can be an array of mixed lt,gt,lte,gte; and if so, it should match the number of elements in the $value array too. 753 | * @param scalar|scalar[] $value the value for the range comparison. Should be an array of same element count if the $operator is also an array. 754 | * @param string $bool must, should, or must_not 755 | * @param boolean $filter to use the filter part instead of the query part 756 | * @param array $params additional query parameters for the range query 757 | * @return QueryBuilder|static|self 758 | */ 759 | public function multiRange(array $keys, $operator, $value, $bool, $filter, array $params = []) { 760 | $subQuery = $this->orWhere($bool); 761 | foreach ($keys as $key) { 762 | $subQuery->range($key, $operator, $value, $bool, true, $params); 763 | } 764 | $subQuery->endSubQuery(); 765 | return $this; 766 | } 767 | 768 | /** 769 | * Creates an AND filter subquery 770 | * 771 | * @param string $bool must, should, or must_not 772 | * @param array $params extra params for and filter 773 | * @return SubQueryBuilder 774 | */ 775 | public function andWhere($bool = "must", array $params = []) { 776 | $callback = function(SubQueryBuilder $subQuery) use ($bool, $params) { 777 | return $this->_endChildSubQuery("and", $subQuery->toArray(), $bool, $params); 778 | }; 779 | return SubQueryBuilder::make($callback); 780 | } 781 | 782 | /** 783 | * Creates an OR filter subquery 784 | * 785 | * @param string $bool must, should, or must_not 786 | * @param array $params extra params for or filter 787 | * @return SubQueryBuilder 788 | */ 789 | public function orWhere($bool = "must", array $params = []) { 790 | $callback = function(SubQueryBuilder $subQuery) use ($bool, $params) { 791 | return $this->_endChildSubQuery("or", $subQuery->toArray(), $bool, $params); 792 | }; 793 | return SubQueryBuilder::make($callback); 794 | } 795 | 796 | /** 797 | * Receives the end signal from the sub query object 798 | * 799 | * @param string $tool [and|or] 800 | * @param array $subQuery 801 | * @param type $bool 802 | * @param array $params 803 | * @return QueryBuilder|static|self 804 | */ 805 | protected function _endChildSubQuery($tool, array $subQuery, $bool, array $params = []) { 806 | $this->addBool([$tool => $subQuery], $bool, true, $params); 807 | return $this; 808 | } 809 | 810 | /** 811 | * Checks and creates the required structure 812 | * 813 | * @param string $key A dot [.] separated path to be created/checked 814 | * @return void 815 | */ 816 | protected function assertInitiated($key) { 817 | $current = &$this->query; 818 | $keys = explode(".", $key); 819 | foreach ($keys as $element) { 820 | if (!array_key_exists($element, $current)) { 821 | $current[$element] = []; 822 | } 823 | $current = &$current[$element]; 824 | } 825 | } 826 | 827 | /** 828 | * Adds a raw must filter part 829 | * 830 | * @param array $query 831 | * @return QueryBuilder|static|self 832 | */ 833 | public function rawMustFilter(array $query) { 834 | $this->addBool($query, "must", true); 835 | return $this; 836 | } 837 | 838 | /** 839 | * Adds a raw must not filter part 840 | * 841 | * @param array $query 842 | * @return QueryBuilder|static|self 843 | */ 844 | public function rawMustNotFilter(array $query) { 845 | $this->addBool($query, "must_not", true); 846 | return $this; 847 | } 848 | 849 | /** 850 | * Adds a raw should filter part 851 | * 852 | * @param array $query 853 | * @return QueryBuilder|static|self 854 | */ 855 | public function rawShouldFilter(array $query) { 856 | $this->addBool($query, "should", true); 857 | return $this; 858 | } 859 | 860 | /** 861 | * Adds a raw must query part 862 | * @param array $query 863 | * @return QueryBuilder|static|self 864 | */ 865 | public function rawMustQuery(array $query) { 866 | $this->addBool($query, "must", false); 867 | return $this; 868 | } 869 | 870 | /** 871 | * Adds a raw must_not query part 872 | * 873 | * @param array $query 874 | * @return QueryBuilder|static|self 875 | */ 876 | public function rawMustNotQuery(array $query) { 877 | $this->addBool($query, "must_not", false); 878 | return $this; 879 | } 880 | 881 | /** 882 | * Adds a raw bool should query part 883 | * 884 | * @param array $query 885 | * @return QueryBuilder|static|self 886 | */ 887 | public function rawShouldQuery(array $query) { 888 | $this->addBool($query, "should", false); 889 | return $this; 890 | } 891 | 892 | /** 893 | * Adds a new bool query part to the query array 894 | * 895 | * @param array $query the bool query part to be added 896 | * @param string $bool the bool group (must, should, must_not) 897 | * @param boolean $filter weither to be added to the filter or the query party 898 | * @param array $params extra parameters for the query part (will be merged into the original part) 899 | * 900 | * @return void 901 | */ 902 | protected function addBool(array $query, $bool, $filter = false, array $params = []) { 903 | $filtered = $filter ? "filter" : "query"; 904 | $key = "query.filtered.{$filtered}.bool.{$bool}"; 905 | if ($filter) { 906 | $this->assertInitiated($key); 907 | } 908 | $this->query["query"]["filtered"][$filtered]["bool"][$bool][] = array_merge_recursive($query, $params); 909 | } 910 | 911 | /** 912 | * Set the from and size (paging) of the results 913 | * 914 | * @param integer $size 915 | * @param integer $from 916 | * @return QueryBuilder|static|self 917 | */ 918 | public function page($size, $from = null) { 919 | if ($from) { 920 | $this->from($from); 921 | } 922 | if ($size) { 923 | $this->size($size); 924 | } 925 | return $this; 926 | } 927 | 928 | /** 929 | * Sets the size of the results 930 | * 931 | * @param integer $size 932 | * @return QueryBuilder|static|self 933 | */ 934 | public function size($size) { 935 | $this->assertInitiated("size"); 936 | $this->query['size'] = $size; 937 | return $this; 938 | } 939 | 940 | /** 941 | * Sets the start index of the results 942 | * 943 | * @param integer $from 944 | * @return QueryBuilder|static|self 945 | */ 946 | public function from($from) { 947 | $this->assertInitiated("from"); 948 | $this->query['from'] = $from; 949 | return $this; 950 | } 951 | 952 | /** 953 | * Adds a row query part[s] to the current query. 954 | * 955 | * Mainly, it is merging recursivly the $query with the current query 956 | * 957 | * @param array $query 958 | * @return QueryBuilder|static|self 959 | */ 960 | public function raw(array $query) { 961 | array_merge_recursive($this->query, $query); 962 | return $this; 963 | } 964 | 965 | protected function makeFilteredQuery(array $queryQuery, $filter = false) { 966 | return $filter ? ["query" => $queryQuery] : $queryQuery; 967 | } 968 | 969 | protected function shouldBeFilter($explicit, $implicit) { 970 | return $implicit === true ? true : ($explicit === false ? false : ($explicit || $implicit ? true : $explicit)); 971 | } 972 | 973 | } 974 | -------------------------------------------------------------------------------- /src/Result.php: -------------------------------------------------------------------------------- 1 | 29 | */ 30 | class Result implements ArrayAccess, Iterator { 31 | 32 | /** 33 | * The result set 34 | * @var array 35 | */ 36 | protected $result; 37 | 38 | /** 39 | * The es client object 40 | * @var Client 41 | */ 42 | protected $esClient; 43 | 44 | /** 45 | * Current element index for current/valid/next/... operations 46 | * @var integer 47 | */ 48 | protected $currentIndex = 0; 49 | 50 | /** 51 | * The class name/pattern for the models when created 52 | * @var string 53 | */ 54 | protected $modelClass = ''; 55 | 56 | /** 57 | * A boolean defines whether to return the document id or the array index 58 | * as keys. 59 | * 60 | * @var boolean 61 | */ 62 | protected $indexKeys = false; 63 | 64 | /** 65 | * Factory method to create the result object 66 | * 67 | * @param array $result 68 | * @param Client $esClient 69 | * @return Result 70 | */ 71 | public static function make(array $result, Client $esClient = null) { 72 | return new static($result, $esClient); 73 | } 74 | 75 | /** 76 | * Constructor method to create the result object 77 | * 78 | * @param array $result 79 | * @param Client $esClient 80 | */ 81 | public function __construct(array $result, Client $esClient = null) { 82 | $this->result = $result; 83 | $this->esClient = $esClient; 84 | } 85 | 86 | /** 87 | * Sets the model class name. When initating a new model object this name 88 | * will be used. 89 | * 90 | * @param string $class 91 | * @return Result 92 | */ 93 | public function setModelClass($class) { 94 | $this->modelClass = $class; 95 | return $this; 96 | } 97 | 98 | /** 99 | * Sets the result to return array index key as the key for key mehtod. 100 | * 101 | * @return Result 102 | */ 103 | public function useIndexKeys() { 104 | $this->indexKeys = true; 105 | return $this; 106 | } 107 | 108 | /** 109 | * Sets the result to return document id as the key for the key method 110 | * 111 | * @return Result 112 | */ 113 | public function useDocumentKeys() { 114 | $this->indexKeys = false; 115 | return $this; 116 | } 117 | 118 | /** 119 | * Returns the number of the hits 120 | * 121 | * @return integer 122 | */ 123 | public function count() { 124 | return $this->result['hits']['total']; 125 | } 126 | 127 | /** 128 | * Returns the highest score in the hists 129 | * 130 | * @return float 131 | */ 132 | public function score() { 133 | return $this->result['hits']['max_score']; 134 | } 135 | 136 | /** 137 | * Returns the raw data array in the hist. 138 | * This is the raw returned hits list from ES. 139 | * 140 | * @return array 141 | */ 142 | public function data() { 143 | return $this->result['hits']['hits']; 144 | } 145 | 146 | /** 147 | * Returns the current model object in the current offset. 148 | * 149 | * @return Model 150 | */ 151 | public function current() { 152 | return $this->offsetGet($this->currentIndex); 153 | } 154 | 155 | /** 156 | * Returns the current array index or document object at the offset. 157 | * 158 | * @see Result::useIndexKeys() 159 | * @see Result::useDocumentKeys() 160 | * @return mixed 161 | */ 162 | public function key() { 163 | if ($this->indexKeys) { 164 | return $this->currentIndex; 165 | } else { 166 | return $this->result['hits']['hits'][$this->currentIndex]['_id']; 167 | } 168 | } 169 | 170 | /** 171 | * Moves the internal current offset to the next element in the array. 172 | */ 173 | public function next() { 174 | ++$this->currentIndex; 175 | } 176 | 177 | /** 178 | * Resets the internal current offset to 0 (the beginning). 179 | */ 180 | public function rewind() { 181 | $this->currentIndex = 0; 182 | } 183 | 184 | /** 185 | * Returns true if the current offset index exists, false otherwise. 186 | * 187 | * @return boolean 188 | */ 189 | public function valid() { 190 | return $this->offsetExists($this->currentIndex); 191 | } 192 | 193 | /** 194 | * Returns true if the $offset exsits in the results array, false otherwise. 195 | * IT uses the array index keys for the offset. 196 | * 197 | * @param integer $offset 198 | * @return boolean 199 | */ 200 | public function offsetExists($offset) { 201 | return array_key_exists($offset, $this->result['hits']['hits']); 202 | } 203 | 204 | /** 205 | * Gets the document object in the $offset 206 | * 207 | * @param integer $offset 208 | * @return Model 209 | */ 210 | public function offsetGet($offset) { 211 | return Model::makeOfType($this->result['hits']['hits'][$offset], $this->modelClass, $this->esClient); 212 | } 213 | 214 | /** 215 | * Not in use. Throws error. 216 | */ 217 | public function offsetSet($offset, $value) { 218 | trigger_error('Can not set offset in ES results', E_USER_ERROR); 219 | } 220 | 221 | /** 222 | * Not in use. Throws error. 223 | */ 224 | public function offsetUnset($offset) { 225 | trigger_error('Can not unset offset in ES results', E_USER_ERROR); 226 | } 227 | 228 | /** 229 | * Returns the first document object in the results array. 230 | * 231 | * @return Model 232 | */ 233 | public function first() { 234 | if ($this->offsetExists(0)) { 235 | return $this->offsetGet(0); 236 | } else { 237 | trigger_error('Can not get null of empty result set', E_USER_WARNING); 238 | return null; 239 | } 240 | } 241 | 242 | /** 243 | * Gets specific value from within the result set using dot notation 244 | * 245 | * The path should be absolution from the beginning, and include the normal 246 | * elastic search structure depending on the query type 247 | * 248 | * @param string $path 249 | */ 250 | public function fetch($path) { 251 | $result = $this->result; 252 | $keys = explode(".", $path); 253 | foreach ($keys as $key) { 254 | if (!array_key_exists($key, $result)) { 255 | return null; 256 | } 257 | $result = $result[$key]; 258 | } 259 | return $result; 260 | } 261 | 262 | public function __get($name) { 263 | return $this->fetch($name); 264 | } 265 | 266 | } 267 | -------------------------------------------------------------------------------- /src/SubQueryBuilder.php: -------------------------------------------------------------------------------- 1 | 25 | * 26 | * @method-static self|static|SubQueryBuilder call(callable $endCallback) 27 | */ 28 | class SubQueryBuilder extends QueryBuilder { 29 | 30 | protected $endCallback; 31 | 32 | protected static function __makeSub(callable $endCallback) { 33 | return new static($endCallback); 34 | } 35 | 36 | public static function __callStatic($name, $arguments) { 37 | switch ($name) { 38 | case 'make': 39 | return static::__makeSub(array_key_exists(0, $arguments) ? $arguments[0] : null); 40 | } 41 | } 42 | 43 | /** 44 | * 45 | * @param callable $endCallback The callback function to call once the sub 46 | * query builder finishes its work. 47 | */ 48 | public function __construct(callable $endCallback) { 49 | $this->endCallback = $endCallback; 50 | } 51 | 52 | protected function addBool(array $query, $bool, $filter = false, array $params = []) { 53 | $this->query[] = array_merge_recursive($query, $params); 54 | } 55 | 56 | /** 57 | * Finishes the sub query builder context and returns to the original query 58 | * builder context. 59 | * 60 | * It will call the provided callable $endCallback function when inistantia- 61 | * ted. 62 | * 63 | * @return QueryBuilder 64 | */ 65 | public function endSubQuery() { 66 | $callback = $this->endCallback; 67 | 68 | return $callback($this); 69 | } 70 | 71 | protected function shouldBeFilter($explicit, $implicit) { 72 | return true; 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /src/TypeQuery.php: -------------------------------------------------------------------------------- 1 | 30 | * 31 | */ 32 | abstract class TypeQuery extends Query implements TypeQueryInterface { 33 | 34 | use TypeQueryTrait; 35 | } 36 | 37 | error_reporting($errorReporting); 38 | -------------------------------------------------------------------------------- /src/TypeQueryInterface.php: -------------------------------------------------------------------------------- 1 | 23 | * 24 | */ 25 | Interface TypeQueryInterface { 26 | 27 | /** 28 | * Defines the type string to be passed to the query/fetch methods in ES 29 | * client 30 | * 31 | * @return string 32 | */ 33 | public function type(); 34 | 35 | /** 36 | * Returns the base model class name to be used. 37 | * 38 | * i.e. if the class full name is \Models\FooModel, this should be Foo 39 | * 40 | * @return string 41 | */ 42 | public function modelClassName(); 43 | } 44 | -------------------------------------------------------------------------------- /src/TypeQueryTrait.php: -------------------------------------------------------------------------------- 1 | 23 | * 24 | */ 25 | trait TypeQueryTrait { 26 | 27 | /** 28 | * Overrides the parent class _all method to omit the type in parameters. 29 | * @see Query::_all for details. 30 | * 31 | * @return Result|Model[] 32 | */ 33 | protected function _all() { 34 | return parent::_all($this->type()); 35 | } 36 | 37 | /** 38 | * Overrides the parent class _query method to omit the type in parameters. 39 | * @see Query::_query for details. 40 | * 41 | * @param array $query 42 | * @return Result|Model[] 43 | */ 44 | protected function _query(array $query = array()) { 45 | return parent::_query($this->type(), $query); 46 | } 47 | 48 | /** 49 | * Overrides the parent class _find method to omit the type in parameters. 50 | * @see Query::_find for details. 51 | * 52 | * @param mixed $id 53 | * @return Result|Model[] 54 | */ 55 | protected function _find($id) { 56 | return parent::_find($this->type(), $id); 57 | } 58 | 59 | /** 60 | * Overrides the parent class _create method to omit the type in parameters. 61 | * @see Query::_create for details. 62 | * 63 | * @param mixed $id 64 | * @return Result|Model[] 65 | */ 66 | protected function _create(array $data, $id = null, array $parameters = []) { 67 | return parent::_create($data, $this->type(), $id, $parameters); 68 | } 69 | 70 | /** 71 | * Creates the full model class name 72 | * 73 | * It uses the query::modelClassNamePattern and typequery::modelClassName 74 | * methods to create the full classname. 75 | * 76 | * @return string 77 | */ 78 | protected function _fullModelClass() { 79 | $baseClassName = $this->modelClassName(); 80 | $classNamePattern = $this->modelClassNamePattern(); 81 | if ($classNamePattern && strpos($classNamePattern, '{type}') !== false) { 82 | $fullClassName = str_replace("{type}", $baseClassName, $classNamePattern); 83 | } elseif ($classNamePattern) { 84 | $fullClassName = "{$classNamePattern}{$baseClassName}"; 85 | } else { 86 | $fullClassName = "\\{$baseClassName}"; 87 | } 88 | return $fullClassName; 89 | } 90 | 91 | /** 92 | * Overrides the parent class _makeResult method to pass the correct model 93 | * class name. 94 | * @see Query::_makeResult for details. 95 | * 96 | * @param array $result 97 | * @return Result|Model[] 98 | */ 99 | protected function _makeResult(array $result) { 100 | return Result::make($result, $this->getClient())->setModelClass($this->_fullModelClass()); 101 | } 102 | 103 | /** 104 | * Overrides the parent class _makeModel method to pass the correct model 105 | * class name. 106 | * @see Query::_makeModel for details. 107 | * 108 | * @param array $source 109 | * @return Model 110 | */ 111 | protected function _makeModel(array $source) { 112 | return Model::makeOfType($source, $this->_fullModelClass(), $this->getClient()); 113 | } 114 | 115 | /** 116 | * Overrides the parent class _delete method to pass the correct type 117 | * @see Query::_delete for details 118 | * 119 | * @param scalar $id 120 | * @return array The elastic command result 121 | */ 122 | protected function _delete($id) { 123 | return parent::_delete($this->type(), $id); 124 | } 125 | 126 | } 127 | -------------------------------------------------------------------------------- /tests/BarModel.php: -------------------------------------------------------------------------------- 1 | toJSON()); 8 | 9 | $builder->emptyQuery(); 10 | $builder->term("name", "Muhannad Shelleh"); 11 | var_dump($builder->toJSON()); 12 | 13 | $builder->emptyQuery(); 14 | $builder->term(["name", "email"], "Muhannad Shelleh"); 15 | var_dump($builder->toJSON()); 16 | 17 | $builder->emptyQuery(); 18 | $builder->term(["name", "email"], ["Muhannad", "Shelleh"]); 19 | var_dump($builder->toJSON()); 20 | 21 | $builder->emptyQuery(); 22 | $builder->orWhere()->where("name", "Shehab", "*")->where("email", "mhh1422", "*")->endSubQuery(); 23 | var_dump($builder->toJSON()); 24 | 25 | $builder->emptyQuery(); 26 | $builder->orWhere()->where("name", "Muhannad Shelleh")->where("email", "muhannad.shelleh@live.com")->endSubQuery(); 27 | var_dump($builder->toJSON()); 28 | 29 | $builder->emptyQuery(); 30 | $builder->orWhere()->where("name", "Muhannad", "=*")->where("email", "*hotmail*", "*=*")->endSubQuery(); 31 | var_dump($builder->toJSON()); 32 | -------------------------------------------------------------------------------- /tests/TestsIndexQuery.php: -------------------------------------------------------------------------------- 1 | [ 23 | 'filtered' => [ 24 | 'filter' => [ 25 | 'term' => ['alive' => false] 26 | ] 27 | ] 28 | ] 29 | ]; 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/case1.php: -------------------------------------------------------------------------------- 1 | name}. My id is {$model->id}, and I am {$model['age']} years old! $extra
"; 29 | } else { 30 | echo "{$id} (null): I am not there!
"; 31 | } 32 | } 33 | 34 | //get all of type 35 | foreach (\Tests\FooTypeQuery::all() as $id => $model) { 36 | dump_me($id, $model); 37 | } 38 | 39 | //find model by id 40 | $ids = ['bar1', 'bar0']; 41 | foreach ($ids as $id) { 42 | $model = \Tests\BarTypeQuery::find($id); 43 | dump_me($id, $model); 44 | } 45 | 46 | //find model by type and id 47 | $id = 'foo1'; 48 | $model = Tests\TestsIndexQuery::find('foo', $id); 49 | dump_me($id, $model); 50 | 51 | //find model by type and id for unclassed one 52 | $id = 'zee1'; 53 | $model = Tests\TestsIndexQuery::find('zee', $id); 54 | dump_me($id, $model); 55 | 56 | //find multiple models 57 | $ids = ['foo0', 'foo1', 'foo2', 'foo3', 'foo4']; 58 | $models = Tests\TestsIndexQuery::find('foo', $ids); 59 | echo "{$models->count()} documents found out of " . count($ids) . " total ids!
"; 60 | foreach ($models as $id => $model) { 61 | dump_me($id, $model, "I am found through mget method internally!"); 62 | } 63 | 64 | //find multiple models 65 | $ids = ['foo0', 'foo1', 'foo2', 'foo3', 'foo4']; 66 | $models = Tests\FooTypeQuery::find($ids); 67 | echo "{$models->count()} documents found out of " . count($ids) . " total ids using the type query!
"; 68 | foreach ($models as $id => $model) { 69 | dump_me($id, $model, "I am found through mget method internally, but using the type query instead!!"); 70 | } 71 | 72 | 73 | //get models by query of type 74 | foreach (\Tests\FooTypeQuery::query([ 75 | "query" => [ 76 | "range" => [ 77 | "age" => [ 78 | "gte" => 30 79 | ] 80 | ] 81 | ] 82 | ]) as $id => $model) { 83 | dump_me($id, $model); 84 | } 85 | 86 | //get models by query for all types 87 | foreach (\Tests\TestsIndexQuery::query(null, [ 88 | "query" => [ 89 | "range" => [ 90 | "age" => [ 91 | "gte" => 30 92 | ] 93 | ] 94 | ] 95 | ]) as $id => $model) { 96 | dump_me($id, $model, "My score is {$model->score} and I am " . ($model->alive ? "alive" : "dead")); 97 | $model->update(['doc' => ['alive' => !$model->alive] + $model->attributes]); 98 | } -------------------------------------------------------------------------------- /tests/case2.php: -------------------------------------------------------------------------------- 1 | 0, 27 | "aggregations" => [ 28 | "alive" => [ 29 | "terms" => [ 30 | "field" => "alive" 31 | ] 32 | ] 33 | ] 34 | ]); 35 | 36 | var_dump($result->fetch('aggregations.alive')); 37 | -------------------------------------------------------------------------------- /tests/case3.php: -------------------------------------------------------------------------------- 1 | name}. My id is {$model->id}, and I am {$model['age']} years old! $extra
"; 27 | } else { 28 | echo "{$id} (null): I am not there!
"; 29 | } 30 | } 31 | 32 | //get all of type 33 | foreach (Tests\ZeeTypeQuery::all() as $id => $model) { 34 | /* @var $model Models\Zee */ 35 | dump_me($id, $model, "I am " . ($model->alive ? "alive" : "dead")); 36 | } 37 | -------------------------------------------------------------------------------- /tests/case4.php: -------------------------------------------------------------------------------- 1 | name}. My id is {$model->id}, and I am {$model['age']} years old! $extra
"; 27 | } else { 28 | echo "{$id} (null): I am not there!
"; 29 | } 30 | } 31 | 32 | //get all of type 33 | var_dump(\Tests\BarTypeQuery::create(['name' => 'Bard', 'id' => 4, 'age' => 8, 'alive' => true], 'zee4')); 34 | var_dump(\Tests\TestsIndexQuery::create(['name' => 'Bard', 'id' => 4, 'age' => 8, 'alive' => true], 'bar', 'bar4')); 35 | var_dump(\Tests\BarTypeQuery::all()->data()); 36 | --------------------------------------------------------------------------------