├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── config └── search.php ├── src ├── Builder.php ├── Factory.php ├── Grammar.php ├── LaravelServiceProvider.php └── Query.php └── tests └── BuildTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: ['https://blog.crcms.cn/reward'] 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | vendor/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 simon 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crcms Elasticsearch 2 | 3 | 4 | [![Latest Stable Version](https://poser.pugx.org/crcms/elasticsearch/v/stable)](https://packagist.org/packages/crcms/elasticsearch) 5 | [![License](https://poser.pugx.org/crcms/elasticsearch/license)](https://packagist.org/packages/crcms/elasticsearch) 6 | [![StyleCI](https://github.styleci.io/repos/100927763/shield?branch=master)](https://github.styleci.io/repos/100927763) 7 | 8 | ## Version Matrix 9 | 10 | | Elasticsearch Version | crcms/elasticsearch Branch | 11 | | --------------------- | ------------------------ | 12 | | >= 7.0 | master(beta unstable) | 13 | | >= 6.0 | 1.* | 14 | | >= 5.0, < 6.0 | 0.* | 15 | 16 | ## Install 17 | 18 | You can install the package via composer: 19 | 20 | ``` 21 | composer require crcms/elasticsearch 22 | ``` 23 | 24 | > Please install if you want to use the latest version `dev-master` 25 | 26 | ## Use 27 | 28 | ### Non-Laravel framework 29 | 30 | ```php 31 | // select config path 32 | $config = require 'search.php'; 33 | $builder = Factory::builder($config); 34 | ``` 35 | 36 | ### Laravel 37 | 38 | Modify `config / app.php` If the version is less <= 5.5 39 | ``` 40 | 'providers' => [ 41 | CrCms\ElasticSearch\LaravelServiceProvider::class, 42 | ] 43 | 44 | ``` 45 | 46 | If you'd like to make configuration changes in the configuration file you can pubish it with the following Aritsan command: 47 | ``` 48 | php artisan vendor:publish --provider="CrCms\ElasticSearch\LaravelServiceProvider" 49 | ``` 50 | 51 | 52 | 53 | ## Quickstart 54 | 55 | ### Create 56 | 57 | ```php 58 | $builder->index('index')->type('type')->create([ 59 | 'key' => 'value' 60 | ]); 61 | 62 | // return a collection 63 | $builder->index('index')->type('type')->createCollection([ 64 | 'key' => 'value' 65 | ]); 66 | ``` 67 | 68 | ### Update 69 | 70 | ```php 71 | $builder->index('index')->type('type')->update([ 72 | 'key' => 'value1' 73 | ]); 74 | 75 | ``` 76 | 77 | ### Delete 78 | 79 | ```php 80 | $builder->index('index')->type('type')->delete($result->_id); 81 | ``` 82 | 83 | ### Select 84 | 85 | ```php 86 | 87 | $builder = $builder->index('index')->type('type'); 88 | 89 | //SQL:select ... where id = 1 limit 1; 90 | $result = $builder->whereTerm('id',1)->first(); 91 | 92 | //SQL:select ... where (key=1 or key=2) and key1=1 93 | $result = $builder->where(function (Query $inQuery) { 94 | $inQuery->whereTerm('key',1)->orWhereTerm('key',2) 95 | })->whereTerm('key1',1)->get(); 96 | 97 | ``` 98 | 99 | ### More 100 | 101 | skip / take 102 | ```php 103 | $builder->take(10)->get(); // or limit(10) 104 | $builder->offset(10)->take(10)->get(); // or skip(10) 105 | ``` 106 | 107 | term query 108 | ```php 109 | $builder->whereTerm('key',value)->first(); 110 | ``` 111 | 112 | match query 113 | ```php 114 | $builder->whereMatch('key',value)->first(); 115 | ``` 116 | 117 | range query 118 | ```php 119 | $builder->whereBetween('key',[value1,value2])->first(); 120 | ``` 121 | 122 | where in query 123 | ```php 124 | $builder->whereIn('key',[value1,value2])->first(); 125 | ``` 126 | 127 | logic query 128 | ```php 129 | $builder->whereTerm('key',value)->orWhereTerm('key2',value)->first(); 130 | ``` 131 | 132 | nested query 133 | ```php 134 | $result = $builder->where(function (Builder $inQuery) { 135 | $inQuery->whereTerm('key',1)->orWhereTerm('key',2) 136 | })->whereTerm('key1',1)->get(); 137 | ``` 138 | 139 | ### Available conditions 140 | 141 | ```php 142 | public function select($columns): self 143 | ``` 144 | 145 | ```php 146 | public function where($column, $operator = null, $value = null, $leaf = 'term', $boolean = 'and'): self 147 | ``` 148 | 149 | 150 | ```php 151 | public function orWhere($field, $operator = null, $value = null, $leaf = 'term'): self 152 | ``` 153 | 154 | ```php 155 | public function whereMatch($field, $value, $boolean = 'and'): self 156 | ``` 157 | 158 | ```php 159 | public function orWhereMatch($field, $value, $boolean = 'and'): self 160 | ``` 161 | 162 | ```php 163 | public function whereTerm($field, $value, $boolean = 'and'): self 164 | ``` 165 | 166 | ```php 167 | public function whereIn($field, array $value) 168 | ``` 169 | 170 | ```php 171 | public function orWhereIn($field, array $value) 172 | ``` 173 | 174 | ```php 175 | public function orWhereTerm($field, $value, $boolean = 'or'): self 176 | ``` 177 | 178 | ```php 179 | public function whereRange($field, $operator = null, $value = null, $boolean = 'and'): self 180 | ``` 181 | 182 | ```php 183 | public function orWhereRange($field, $operator = null, $value = null): self 184 | ``` 185 | 186 | ```php 187 | public function whereBetween($field, array $values, $boolean = 'and'): self 188 | ``` 189 | ```php 190 | public function whereNotBetween($field, array $values): self 191 | ``` 192 | ```php 193 | public function orWhereNotBetween(string $field, array $values): self 194 | ``` 195 | ```php 196 | public function whereExists($field, $boolean = 'and'): self 197 | ``` 198 | ```php 199 | public function whereNotExists($field, $boolean = 'and'): self 200 | ``` 201 | 202 | ```php 203 | public function orWhereBetween($field, array $values): self 204 | ``` 205 | 206 | ```php 207 | public function orderBy(string $field, $sort): self 208 | ``` 209 | 210 | ```php 211 | public function scroll(string $scroll): self 212 | ``` 213 | 214 | ```php 215 | public function aggBy($field, $type): self 216 | ``` 217 | 218 | ```php 219 | public function select($columns): self 220 | ``` 221 | 222 | ### Result Method 223 | ```php 224 | public function get(): Collection 225 | ``` 226 | 227 | ```php 228 | public function paginate(int $page, int $perPage = 15): Collection 229 | ``` 230 | 231 | ```php 232 | public function first() 233 | ``` 234 | 235 | ```php 236 | public function byId($id) 237 | ``` 238 | 239 | ```php 240 | public function byIdOrFail($id): stdClass 241 | ``` 242 | 243 | ```php 244 | public function chunk(callable $callback, $limit = 2000, $scroll = '10m') 245 | ``` 246 | 247 | ```php 248 | public function create(array $data, $id = null, $key = 'id'): stdClass 249 | ``` 250 | 251 | ```php 252 | public function update($id, array $data): bool 253 | ``` 254 | 255 | ```php 256 | public function delete($id) 257 | ``` 258 | 259 | ```php 260 | public function count(): int 261 | ``` 262 | 263 | ### Log 264 | 265 | ```php 266 | //open log 267 | $builder->enableQueryLog(); 268 | 269 | //all query log 270 | dump($build->getQueryLog()); 271 | 272 | //last query log 273 | dump($build->getLastQueryLog()); 274 | ``` 275 | 276 | ### Elastisearch object 277 | 278 | ```php 279 | getElasticSearch() // or search() 280 | ``` 281 | 282 | > If you want to expand more, you can use this method, call https://github.com/elastic/elasticsearch-php 283 | 284 | ## Other 285 | For more examples, please see test cases 286 | https://github.com/crcms/elasticsearch/blob/master/tests/BuildTest.php 287 | ## License 288 | [MIT license](https://opensource.org/licenses/MIT) 289 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "crcms/elasticsearch", 3 | "description": "Use SQL statements to query elasticsearch", 4 | "keywords": [ 5 | "elasticsearch", 6 | "laravel", 7 | "sql", 8 | "orm" 9 | ], 10 | "homepage": "https://github.com/crcms/elasticsearch", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "simon", 15 | "email": "simon@crcms.cn" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=7.3", 20 | "illuminate/support": "^5.3|^6.0|^7.0|^8.0", 21 | "elasticsearch/elasticsearch": "~7.0", 22 | "ramsey/uuid": "^4.1" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^9.4", 26 | "symfony/var-dumper": "^5.2" 27 | }, 28 | "autoload": { 29 | "psr-4": { 30 | "CrCms\\ElasticSearch\\": "src/" 31 | } 32 | }, 33 | "autoload-dev": { 34 | "psr-4": { 35 | "CrCms\\ElasticSearch\\Test\\": "tests/" 36 | } 37 | }, 38 | "extra": { 39 | "laravel": { 40 | "providers": [ 41 | "CrCms\\ElasticSearch\\LaravelServiceProvider" 42 | ], 43 | "aliases": { 44 | } 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /config/search.php: -------------------------------------------------------------------------------- 1 | [], 18 | 19 | /* 20 | |-------------------------------------------------------------------------- 21 | | Elasticsearch Connection Pool 22 | |-------------------------------------------------------------------------- 23 | | 24 | | Choose the following 25 | | 26 | | Elasticsearch\ConnectionPool\StaticNoPingConnectionPool::class 27 | | Elasticsearch\ConnectionPool\SimpleConnectionPool::class 28 | | Elasticsearch\ConnectionPool\SniffingConnectionPool::class 29 | | Elasticsearch\ConnectionPool\StaticConnectionPool::class 30 | | 31 | */ 32 | 33 | 'connection_pool' => Elasticsearch\ConnectionPool\StaticNoPingConnectionPool::class, 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Elasticsearch Selector 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Setting the Connection Selector 41 | | 42 | | Elasticsearch\ConnectionPool\Selectors\StickyRoundRobinSelector::class 43 | | Elasticsearch\ConnectionPool\Selectors\RoundRobinSelector::class 44 | | Elasticsearch\ConnectionPool\Selectors\RandomSelector::class 45 | | 46 | */ 47 | 48 | 'selector' => Elasticsearch\ConnectionPool\Selectors\StickyRoundRobinSelector::class, 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Open elasticsearch log 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Set whether the log to open the record 56 | | 57 | */ 58 | 59 | 'open_log' => false, 60 | ]; 61 | -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | query = $query; 67 | } 68 | 69 | /** 70 | * @return void 71 | */ 72 | public function resetQuery(): void 73 | { 74 | $this->query = $this->query->newQuery(); 75 | } 76 | 77 | /** 78 | * @return object|null 79 | */ 80 | public function first(): ?object 81 | { 82 | $this->query->limit(1); 83 | 84 | return $this->get()->first(); 85 | } 86 | 87 | /** 88 | * @return Collection 89 | */ 90 | public function get(): Collection 91 | { 92 | return $this->metaData($this->getOriginal()); 93 | } 94 | 95 | /** 96 | * @return array 97 | */ 98 | public function getOriginal(): array 99 | { 100 | return $this->runQuery($this->query->getGrammar()->compileSelect($this->query), 'search'); 101 | } 102 | 103 | /** 104 | * @param int $page 105 | * @param int $perPage 106 | * 107 | * @return Collection 108 | */ 109 | public function paginate(int $page, int $perPage = 15): Collection 110 | { 111 | $from = (($page * $perPage) - $perPage); 112 | 113 | if (empty($this->query->offset)) { 114 | $this->query->offset($from); 115 | } 116 | 117 | if (empty($this->query->limit)) { 118 | $this->query->limit($perPage); 119 | } 120 | 121 | $results = $this->runQuery($this->query->getGrammar()->compileSelect($this->query)); 122 | 123 | $data = $this->metaData($results); 124 | 125 | $maxPage = intval(ceil($results['hits']['total']['value'] / $perPage)); 126 | 127 | return Collection::make([ 128 | 'total' => $results['hits']['total']['value'], 129 | 'per_page' => $perPage, 130 | 'current_page' => $page, 131 | 'next_page' => $page < $maxPage ? $page + 1 : $maxPage, 132 | //'last_page' => $maxPage, 133 | 'total_pages' => $maxPage, 134 | 'from' => $from, 135 | 'to' => $from + $perPage, 136 | 'data' => $data, 137 | ]); 138 | } 139 | 140 | /** 141 | * @param string|int $id 142 | * 143 | * @return null|object 144 | */ 145 | public function byId($id): ?object 146 | { 147 | $result = $this->query->runQuery( 148 | $this->query->whereTerm('_id', $id)->getGrammar()->compileSelect($this->query) 149 | ); 150 | 151 | return isset($result['hits']['hits'][0]) ? 152 | $this->sourceToObject($result['hits']['hits'][0]) : 153 | null; 154 | } 155 | 156 | /** 157 | * @param string|int $id 158 | * 159 | * @return object 160 | */ 161 | public function byIdOrFail($id): object 162 | { 163 | $result = $this->byId($id); 164 | 165 | if (empty($result)) { 166 | throw new RuntimeException('Resource not found by id:'.$id); 167 | } 168 | 169 | return $result; 170 | } 171 | 172 | /** 173 | * @param callable $callback 174 | * @param int $limit 175 | * @param string $scroll 176 | * 177 | * @return bool 178 | */ 179 | public function chunk(callable $callback, $limit = 2000, $scroll = '10m') 180 | { 181 | if (empty($this->query->scroll)) { 182 | $this->query->scroll($scroll); 183 | } else { 184 | $scroll = $this->query->scroll; 185 | } 186 | 187 | if (empty($this->query->limit)) { 188 | $this->query->limit($limit); 189 | } else { 190 | $limit = $this->query->limit; 191 | } 192 | 193 | $condition = $this->query->getGrammar()->compileSelect($this->query); 194 | $results = $this->runQuery($condition, 'search'); 195 | 196 | if ($results['hits']['total']['value'] === 0) { 197 | return; 198 | } 199 | 200 | // First total eq limit 201 | $total = $limit; 202 | 203 | $whileNum = intval(floor($results['hits']['total']['value'] / $total)); 204 | 205 | do { 206 | if (call_user_func($callback, $this->metaData($results)) === false) { 207 | return false; 208 | } 209 | 210 | $results = $this->runQuery(['scroll_id' => $results['_scroll_id'], 'scroll' => $scroll], 'scroll'); 211 | 212 | $total += count($results['hits']['hits']); 213 | } while ($whileNum--); 214 | } 215 | 216 | /** 217 | * @param array $data 218 | * @param string|int|null $id 219 | * @param string $key 220 | * 221 | * @return object 222 | */ 223 | public function create(array $data, $id = null, $key = 'id'): object 224 | { 225 | $id = $id ? $id : (isset($data[$key]) ? $data[$key] : Uuid::uuid6()->toString()); 226 | 227 | $result = $this->runQuery( 228 | $this->query->getGrammar()->compileCreate($this->query, $id, $data), 229 | 'create' 230 | ); 231 | 232 | if (!isset($result['result']) || $result['result'] !== 'created') { 233 | throw new RunTimeException('Create error, params: '.json_encode($this->query->getLastQueryLog())); 234 | } 235 | 236 | $data['_id'] = $id; 237 | $data['_result'] = $result; 238 | 239 | return (object) $data; 240 | } 241 | 242 | /** 243 | * @param array $data 244 | * @param string|int|null $id 245 | * @param string $key 246 | * 247 | * @return Collection 248 | */ 249 | public function createCollection(array $data, $id = null, $key = 'id'): Collection 250 | { 251 | return Collection::make($this->create($data, $id, $key)); 252 | } 253 | 254 | /** 255 | * @param string|int $id 256 | * @param array $data 257 | * 258 | * @return bool 259 | */ 260 | public function update($id, array $data): bool 261 | { 262 | $result = $this->runQuery($this->query->getGrammar()->compileUpdate($this->query, $id, $data), 'update'); 263 | 264 | if (!isset($result['result']) || $result['result'] !== 'updated') { 265 | throw new RunTimeException('Update error params: '.json_encode($this->query->getLastQueryLog())); 266 | } 267 | 268 | return true; 269 | } 270 | 271 | /** 272 | * @param string|int $id 273 | * 274 | * @return bool 275 | */ 276 | public function delete($id): bool 277 | { 278 | $result = $this->runQuery($this->query->getGrammar()->compileDelete($this->query, $id), 'delete'); 279 | 280 | if (!isset($result['result']) || $result['result'] !== 'deleted') { 281 | throw new RunTimeException('Delete error params:'.json_encode($this->query->getLastQueryLog())); 282 | } 283 | 284 | return true; 285 | } 286 | 287 | /** 288 | * @return int 289 | */ 290 | public function count(): int 291 | { 292 | $result = $this->runQuery($this->query->getGrammar()->compileSelect($this->query), 'count'); 293 | 294 | return $result['count']; 295 | } 296 | 297 | /** 298 | * @param array $params 299 | * @param string $method 300 | * 301 | * @return mixed 302 | */ 303 | public function runQuery(array $params, string $method = 'search') 304 | { 305 | if ($this->enableQueryLog) { 306 | $this->queryLogs[] = $params; 307 | } 308 | 309 | return tap(call_user_func([$this->query->getElasticSearch(), $method], $params), function () { 310 | $this->resetQuery(); 311 | }); 312 | } 313 | 314 | /** 315 | * @return Builder 316 | */ 317 | public function enableQueryLog(): self 318 | { 319 | $this->enableQueryLog = true; 320 | 321 | return $this; 322 | } 323 | 324 | /** 325 | * @return Builder 326 | */ 327 | public function disableQueryLog(): self 328 | { 329 | $this->enableQueryLog = false; 330 | 331 | return $this; 332 | } 333 | 334 | /** 335 | * @return array 336 | */ 337 | public function getQueryLog(): array 338 | { 339 | return $this->queryLogs; 340 | } 341 | 342 | /** 343 | * @return array 344 | */ 345 | public function getLastQueryLog(): array 346 | { 347 | return Arr::last($this->queryLogs); 348 | } 349 | 350 | /** 351 | * @param array $results 352 | * 353 | * @return Collection 354 | */ 355 | protected function metaData(array $results): Collection 356 | { 357 | return Collection::make($results['hits']['hits'])->map(function ($hit) { 358 | return $this->sourceToObject($hit); 359 | }); 360 | } 361 | 362 | /** 363 | * @param array $result 364 | * 365 | * @return object 366 | */ 367 | protected function sourceToObject(array $result): object 368 | { 369 | return (object) array_merge($result['_source'], ['_id' => $result['_id'], '_score' => $result['_score']]); 370 | } 371 | 372 | /** 373 | * @param string $name 374 | * @param array $arguments 375 | * 376 | * @return mixed 377 | */ 378 | public function __call(string $name, array $arguments) 379 | { 380 | if (method_exists($this->query, $name)) { 381 | $query = call_user_func_array([$this->query, $name], $arguments); 382 | // If the query instance is returned, it is managed 383 | if ($query instanceof $this->query) { 384 | return $this; 385 | } 386 | 387 | return $query; 388 | } 389 | 390 | throw new BadMethodCallException(sprintf('The method[%s] not found', $name)); 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | setConnectionPool($config['connection_pool']) 39 | ->setSelector($config['selector']) 40 | ->setHosts($config['hosts']); 41 | 42 | if ($config['open_log']) { 43 | $clientBuilder->setLogger( 44 | $logger ? $logger : new EmptyLogger() 45 | ); 46 | } 47 | 48 | return $clientBuilder->build(); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Grammar.php: -------------------------------------------------------------------------------- 1 | 'columns', 16 | 'query' => 'wheres', 17 | 'aggs', 18 | 'sort' => 'orders', 19 | 'size' => 'limit', 20 | 'from' => 'offset', 21 | 'index' => 'index', 22 | 'type' => 'type', 23 | 'scroll' => 'scroll', 24 | ]; 25 | 26 | /** 27 | * @param Query $builder 28 | * 29 | * @return int 30 | */ 31 | public function compileOffset(Query $builder): int 32 | { 33 | return $builder->offset; 34 | } 35 | 36 | /** 37 | * @param Query $builder 38 | * 39 | * @return int 40 | */ 41 | public function compileLimit(Query $builder): int 42 | { 43 | return $builder->limit; 44 | } 45 | 46 | /** 47 | * @param Query $builder 48 | * 49 | * @return string 50 | */ 51 | public function compileScroll(Query $builder): string 52 | { 53 | return $builder->scroll; 54 | } 55 | 56 | /** 57 | * @param Query $builder 58 | * 59 | * @return array 60 | */ 61 | public function compileSelect(Query $builder) 62 | { 63 | $body = $this->compileComponents($builder); 64 | $index = Arr::pull($body, 'index'); 65 | $type = Arr::pull($body, 'type'); 66 | $scroll = Arr::pull($body, 'scroll'); 67 | $params = ['body' => $body, 'index' => $index, 'type' => $type]; 68 | if ($scroll) { 69 | $params['scroll'] = $scroll; 70 | } 71 | 72 | return $params; 73 | } 74 | 75 | /** 76 | * @param Query $builder 77 | * @param $id 78 | * @param array $data 79 | * 80 | * @return array 81 | */ 82 | public function compileCreate(Query $builder, $id, array $data): array 83 | { 84 | return array_merge([ 85 | 'id' => $id, 86 | 'body' => $data, 87 | ], $this->compileComponents($builder)); 88 | } 89 | 90 | /** 91 | * @param Query $builder 92 | * @param $id 93 | * 94 | * @return array 95 | */ 96 | public function compileDelete(Query $builder, $id): array 97 | { 98 | return array_merge([ 99 | 'id' => $id, 100 | ], $this->compileComponents($builder)); 101 | } 102 | 103 | /** 104 | * @param Query $builder 105 | * @param $id 106 | * @param array $data 107 | * 108 | * @return array 109 | */ 110 | public function compileUpdate(Query $builder, $id, array $data): array 111 | { 112 | return array_merge([ 113 | 'id' => $id, 114 | 'body' => ['doc' => $data], 115 | ], $this->compileComponents($builder)); 116 | } 117 | 118 | /** 119 | * @param Query $builder 120 | * 121 | * @return array 122 | */ 123 | public function compileAggs(Query $builder): array 124 | { 125 | $aggs = []; 126 | 127 | foreach ($builder->aggs as $field => $aggItem) { 128 | if (is_array($aggItem)) { 129 | $aggs[] = $aggItem; 130 | } else { 131 | $aggs[$field.'_'.$aggItem] = [$aggItem => ['field' => $field]]; 132 | } 133 | } 134 | 135 | return $aggs; 136 | } 137 | 138 | /** 139 | * @param Query $builder 140 | * 141 | * @return array 142 | */ 143 | public function compileColumns(Query $builder): array 144 | { 145 | return $builder->columns; 146 | } 147 | 148 | /** 149 | * @param Query $builder 150 | * 151 | * @return string 152 | */ 153 | public function compileIndex(Query $builder): string 154 | { 155 | return is_array($builder->index) ? implode(',', $builder->index) : $builder->index; 156 | } 157 | 158 | /** 159 | * @param Query $builder 160 | * 161 | * @return string 162 | */ 163 | public function compileType(Query $builder): string 164 | { 165 | return $builder->type; 166 | } 167 | 168 | /** 169 | * @param Query $builder 170 | * 171 | * @return array 172 | */ 173 | public function compileOrders(Query $builder): array 174 | { 175 | $orders = []; 176 | 177 | foreach ($builder->orders as $field => $orderItem) { 178 | $orders[$field] = is_array($orderItem) ? $orderItem : ['order' => $orderItem]; 179 | } 180 | 181 | return $orders; 182 | } 183 | 184 | /** 185 | * @param Query $builder 186 | * 187 | * @return array 188 | */ 189 | protected function compileWheres(Query $builder): array 190 | { 191 | $whereGroups = $this->wherePriorityGroup($builder->wheres); 192 | 193 | $operation = count($whereGroups) === 1 ? 'must' : 'should'; 194 | 195 | $bool = []; 196 | 197 | foreach ($whereGroups as $wheres) { 198 | $must = []; 199 | $mustNot = []; 200 | foreach ($wheres as $where) { 201 | if ($where['type'] === 'Nested') { 202 | $must[] = $this->compileWheres($where['query']); 203 | } else { 204 | if ($where['operator'] == 'ne') { 205 | $mustNot[] = $this->whereLeaf($where['leaf'], $where['column'], $where['operator'], $where['value']); 206 | } else { 207 | $must[] = $this->whereLeaf($where['leaf'], $where['column'], $where['operator'], $where['value']); 208 | } 209 | } 210 | } 211 | 212 | if (!empty($must)) { 213 | $bool['bool'][$operation][] = count($must) === 1 ? array_shift($must) : ['bool' => ['must' => $must]]; 214 | } 215 | if (!empty($mustNot)) { 216 | if ($operation == 'should') { 217 | foreach ($mustNot as $not) { 218 | $bool['bool'][$operation][] = ['bool'=>['must_not'=>$not]]; 219 | } 220 | } else { 221 | $bool['bool']['must_not'] = $mustNot; 222 | } 223 | } 224 | } 225 | 226 | return $bool; 227 | } 228 | 229 | /** 230 | * @param string $leaf 231 | * @param string $column 232 | * @param string|null $operator 233 | * @param $value 234 | * 235 | * @return array 236 | */ 237 | protected function whereLeaf(string $leaf, string $column, string $operator = null, $value): array 238 | { 239 | if (strpos($column, '@') !== false) { 240 | $columnArr = explode('@', $column); 241 | $ret = ['nested'=>['path'=>$columnArr[0]]]; 242 | $ret['nested']['query']['bool']['must'][] = $this->whereLeaf($leaf, implode('.', $columnArr), $operator, $value); 243 | 244 | return $ret; 245 | } 246 | if (in_array($leaf, ['term', 'match', 'terms', 'match_phrase'], true)) { 247 | return [$leaf => [$column => $value]]; 248 | } elseif ($leaf === 'range') { 249 | return [$leaf => [ 250 | $column => is_array($value) ? $value : [$operator => $value], 251 | ]]; 252 | } elseif ($leaf === 'multi_match') { 253 | return ['multi_match' => [ 254 | 'query' => $value, 255 | 'fields' => (array) $column, 256 | 'type' => 'phrase', 257 | ], 258 | ]; 259 | } elseif ($leaf === 'wildcard') { 260 | return ['wildcard' => [ 261 | $column => '*'.$value.'*', 262 | ], 263 | ]; 264 | } elseif ($leaf === 'exists') { 265 | return ['exists' => [ 266 | 'field' => $column, 267 | ]]; 268 | } 269 | } 270 | 271 | /** 272 | * @param array $wheres 273 | * 274 | * @return array 275 | */ 276 | protected function wherePriorityGroup(array $wheres): array 277 | { 278 | //get "or" index from array 279 | $orIndex = (array) array_keys(array_map(function ($where) { 280 | return $where['boolean']; 281 | }, $wheres), 'or'); 282 | 283 | $lastIndex = $initIndex = 0; 284 | $group = []; 285 | foreach ($orIndex as $index) { 286 | $group[] = array_slice($wheres, $initIndex, $index - $initIndex); 287 | $initIndex = $index; 288 | $lastIndex = $index; 289 | } 290 | 291 | $group[] = array_slice($wheres, $lastIndex); 292 | 293 | return $group; 294 | } 295 | 296 | /** 297 | * @param Query $query 298 | * 299 | * @return array 300 | */ 301 | protected function compileComponents(Query $query): array 302 | { 303 | $body = []; 304 | 305 | foreach ($this->selectComponents as $key => $component) { 306 | if (!empty($query->$component)) { 307 | $method = 'compile'.ucfirst($component); 308 | 309 | $body[is_numeric($key) ? $component : $key] = $this->$method($query, $query->$component); 310 | } 311 | } 312 | 313 | return $body; 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /src/LaravelServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 22 | $this->packagePath.'config' => config_path(), 23 | ]); 24 | } 25 | 26 | /** 27 | * @return void 28 | */ 29 | public function register() 30 | { 31 | //merge config 32 | $this->mergeConfig(); 33 | 34 | $this->bindBuilder(); 35 | } 36 | 37 | /** 38 | * @return void 39 | */ 40 | protected function bindBuilder(): void 41 | { 42 | $this->app->singleton(Builder::class, function ($app) { 43 | return Factory::builder($app->make('config')->get('search'), $app->make('log')); 44 | }); 45 | } 46 | 47 | /** 48 | * @return void 49 | */ 50 | protected function mergeConfig(): void 51 | { 52 | if ($this->isLumen()) { 53 | $this->app->configure('search'); 54 | } 55 | 56 | $this->mergeConfigFrom($this->packagePath.'config/search.php', 'search'); 57 | } 58 | 59 | /** 60 | * isLumen. 61 | * 62 | * @return bool 63 | */ 64 | protected function isLumen(): bool 65 | { 66 | return $this->app instanceof \Laravel\Lumen\Application; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Query.php: -------------------------------------------------------------------------------- 1 | 'eq', 62 | '>' => 'gt', 63 | '>=' => 'gte', 64 | '<' => 'lt', 65 | '<=' => 'lte', 66 | '!=' => 'ne', 67 | ]; 68 | 69 | /** 70 | * @var Grammar 71 | */ 72 | protected $grammar; 73 | 74 | /** 75 | * @var Client 76 | */ 77 | protected $elasticsearch; 78 | 79 | /** 80 | * @var array 81 | */ 82 | protected $config; 83 | 84 | /** 85 | * @param Grammar $grammar 86 | * @param Client $client 87 | */ 88 | public function __construct(Grammar $grammar, Client $client) 89 | { 90 | $this->setGrammar($grammar); 91 | $this->setElasticSearch($client); 92 | } 93 | 94 | /** 95 | * @param string|array $index 96 | * 97 | * @return Query 98 | */ 99 | public function index($index): self 100 | { 101 | $this->index = $index; 102 | 103 | return $this; 104 | } 105 | 106 | /** 107 | * @param string $type 108 | * 109 | * @return Query 110 | */ 111 | public function type($type): self 112 | { 113 | $this->type = $type; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * @param int $value 120 | * 121 | * @return Query 122 | */ 123 | public function limit(int $value): self 124 | { 125 | $this->limit = $value; 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * @param int $value 132 | * 133 | * @return Query 134 | */ 135 | public function take(int $value): self 136 | { 137 | return $this->limit($value); 138 | } 139 | 140 | /** 141 | * @param int $value 142 | * 143 | * @return Query 144 | */ 145 | public function offset(int $value): self 146 | { 147 | $this->offset = $value; 148 | 149 | return $this; 150 | } 151 | 152 | /** 153 | * @param int $value 154 | * 155 | * @return Query 156 | */ 157 | public function skip(int $value): self 158 | { 159 | return $this->offset($value); 160 | } 161 | 162 | /** 163 | * @param string $field 164 | * @param $sort 165 | * 166 | * @return Query 167 | */ 168 | public function orderBy(string $field, $sort): self 169 | { 170 | $this->orders[$field] = $sort; 171 | 172 | return $this; 173 | } 174 | 175 | /** 176 | * @param string|array $field 177 | * @param $type 178 | * 179 | * @return Query 180 | */ 181 | public function aggBy($field, $type = null): self 182 | { 183 | is_array($field) ? 184 | $this->aggs[] = $field : 185 | $this->aggs[$field] = $type; 186 | 187 | return $this; 188 | } 189 | 190 | /** 191 | * @param string $scroll 192 | * 193 | * @return Query 194 | */ 195 | public function scroll(string $scroll): self 196 | { 197 | $this->scroll = $scroll; 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * @param string|array $columns 204 | * 205 | * @return Query 206 | */ 207 | public function select($columns): self 208 | { 209 | $this->columns = is_array($columns) ? $columns : func_get_args(); 210 | 211 | return $this; 212 | } 213 | 214 | /** 215 | * @param $field 216 | * @param $value 217 | * @param string $boolean 218 | * 219 | * @return Query 220 | */ 221 | public function whereMatch($field, $value, $boolean = 'and'): self 222 | { 223 | return $this->where($field, '=', $value, 'match', $boolean); 224 | } 225 | 226 | /** 227 | * @param $field 228 | * @param $value 229 | * @param string $boolean 230 | * 231 | * @return Query 232 | */ 233 | public function orWhereMatch($field, $value, $boolean = 'or'): self 234 | { 235 | return $this->whereMatch($field, $value, $boolean); 236 | } 237 | 238 | /** 239 | * @param $field 240 | * @param $value 241 | * @param string $boolean 242 | * 243 | * @return Query 244 | */ 245 | public function whereTerm($field, $value, $boolean = 'and'): self 246 | { 247 | return $this->where($field, '=', $value, 'term', $boolean); 248 | } 249 | 250 | /** 251 | * @param $field 252 | * @param array $value 253 | * 254 | * @return Query 255 | */ 256 | public function whereIn($field, array $value): self 257 | { 258 | return $this->where(function (self $query) use ($field, $value) { 259 | array_map(function ($item) use ($query, $field) { 260 | $query->orWhereTerm($field, $item); 261 | }, $value); 262 | }); 263 | } 264 | 265 | /** 266 | * @param $field 267 | * @param array $value 268 | * 269 | * @return Query 270 | */ 271 | public function orWhereIn($field, array $value): self 272 | { 273 | return $this->orWhere(function (self $query) use ($field, $value) { 274 | array_map(function ($item) use ($query, $field) { 275 | $query->orWhereTerm($field, $item); 276 | }, $value); 277 | }); 278 | } 279 | 280 | /** 281 | * @param $field 282 | * @param $value 283 | * @param string $boolean 284 | * 285 | * @return Query 286 | */ 287 | public function orWhereTerm($field, $value, $boolean = 'or'): self 288 | { 289 | return $this->whereTerm($field, $value, $boolean); 290 | } 291 | 292 | /** 293 | * @param $field 294 | * @param null $operator 295 | * @param null $value 296 | * @param string $boolean 297 | * 298 | * @return Query 299 | */ 300 | public function whereRange($field, $operator = null, $value = null, $boolean = 'and'): self 301 | { 302 | return $this->where($field, $operator, $value, 'range', $boolean); 303 | } 304 | 305 | /** 306 | * @param $field 307 | * @param null $operator 308 | * @param null $value 309 | * 310 | * @return Query 311 | */ 312 | public function orWhereRange($field, $operator = null, $value = null): self 313 | { 314 | return $this->where($field, $operator, $value, 'or'); 315 | } 316 | 317 | /** 318 | * @param $field 319 | * @param array $values 320 | * @param string $boolean 321 | * 322 | * @return Query 323 | */ 324 | public function whereBetween($field, array $values, $boolean = 'and'): self 325 | { 326 | return $this->where($field, null, $values, 'range', $boolean); 327 | } 328 | 329 | /** 330 | * @param $field 331 | * @param array $values 332 | * 333 | * @return Query 334 | */ 335 | public function orWhereBetween($field, array $values): self 336 | { 337 | return $this->whereBetween($field, $values, 'or'); 338 | } 339 | 340 | /** 341 | * @param $field 342 | * @param array $values 343 | * @param string $boolean 344 | * 345 | * @return Query 346 | */ 347 | public function whereNotBetween($field, array $values, $boolean = 'and'): self 348 | { 349 | return $this->where($field, '!=', $values, 'range', $boolean); 350 | } 351 | 352 | /** 353 | * @param $field 354 | * @param array $values 355 | * 356 | * @return Query 357 | */ 358 | public function orWhereNotBetween($field, array $values): self 359 | { 360 | return $this->whereNotBetween($field, $values, 'or'); 361 | } 362 | 363 | /** 364 | * @param $field 365 | * @param string $boolean 366 | * 367 | * @return Query 368 | */ 369 | public function whereExists($field, $boolean = 'and'): self 370 | { 371 | return $this->where($field, '=', '', 'exists', $boolean); 372 | } 373 | 374 | /** 375 | * @param $field 376 | * @param string $boolean 377 | * 378 | * @return Query 379 | */ 380 | public function whereNotExists($field, $boolean = 'and'): self 381 | { 382 | return $this->where($field, '!=', '', 'exists', $boolean); 383 | } 384 | 385 | /** 386 | * @param Closure|string $column 387 | * @param string|null $operator 388 | * @param string|null $value 389 | * @param string $leaf 390 | * @param string $boolean 391 | * 392 | * @return Query 393 | */ 394 | public function where($column, $operator = null, $value = null, string $leaf = 'term', string $boolean = 'and'): self 395 | { 396 | if ($column instanceof Closure) { 397 | return $this->whereNested($column, $boolean); 398 | } 399 | 400 | if (func_num_args() === 2) { 401 | list($value, $operator) = [$operator, '=']; 402 | } 403 | 404 | if (is_array($operator)) { 405 | list($value, $operator) = [$operator, null]; 406 | } 407 | 408 | if (in_array($operator, ['>=', '>', '<=', '<'])) { 409 | $leaf = 'range'; 410 | } 411 | 412 | if (is_array($value) && $leaf === 'range') { 413 | $value = [ 414 | $this->operators['>='] => $value[0], 415 | $this->operators['<='] => $value[1], 416 | ]; 417 | } 418 | 419 | $type = 'Basic'; 420 | 421 | $operator = $operator ? $this->operators[$operator] : $operator; 422 | 423 | $this->wheres[] = compact( 424 | 'type', 425 | 'column', 426 | 'leaf', 427 | 'value', 428 | 'boolean', 429 | 'operator' 430 | ); 431 | 432 | return $this; 433 | } 434 | 435 | /** 436 | * @param $field 437 | * @param null $operator 438 | * @param null $value 439 | * @param string $leaf 440 | * 441 | * @return Query 442 | */ 443 | public function orWhere($field, $operator = null, $value = null, $leaf = 'term'): self 444 | { 445 | if (func_num_args() === 2) { 446 | list($value, $operator) = [$operator, '=']; 447 | } 448 | 449 | return $this->where($field, $operator, $value, $leaf, 'or'); 450 | } 451 | 452 | /** 453 | * @param Closure $callback 454 | * @param string $boolean 455 | * 456 | * @return Query 457 | */ 458 | public function whereNested(Closure $callback, string $boolean): self 459 | { 460 | $query = $this->newQuery(); 461 | 462 | call_user_func($callback, $query); 463 | 464 | return $this->addNestedWhereQuery($query, $boolean); 465 | } 466 | 467 | /** 468 | * @return Query 469 | */ 470 | public function newQuery(): self 471 | { 472 | return new static($this->grammar, $this->elasticsearch); 473 | } 474 | 475 | /** 476 | * @return Grammar 477 | */ 478 | public function getGrammar(): Grammar 479 | { 480 | return $this->grammar; 481 | } 482 | 483 | /** 484 | * @param Grammar $grammar 485 | * 486 | * @return $this 487 | */ 488 | public function setGrammar(Grammar $grammar) 489 | { 490 | $this->grammar = $grammar; 491 | 492 | return $this; 493 | } 494 | 495 | /** 496 | * @param Client $client 497 | * 498 | * @return $this 499 | */ 500 | public function setElasticSearch(Client $client) 501 | { 502 | $this->elasticsearch = $client; 503 | 504 | return $this; 505 | } 506 | 507 | /** 508 | * @return Client 509 | */ 510 | public function getElasticSearch(): Client 511 | { 512 | return $this->elasticsearch; 513 | } 514 | 515 | /** 516 | * @return Client 517 | */ 518 | public function search(): Client 519 | { 520 | return $this->getElasticSearch(); 521 | } 522 | 523 | /** 524 | * @param Query $query 525 | * @param string $boolean 526 | * 527 | * @return Query 528 | */ 529 | protected function addNestedWhereQuery(Query $query, string $boolean = 'and'): self 530 | { 531 | if (count($query->wheres)) { 532 | $type = 'Nested'; 533 | $this->wheres[] = compact('type', 'query', 'boolean'); 534 | } 535 | 536 | return $this; 537 | } 538 | } 539 | -------------------------------------------------------------------------------- /tests/BuildTest.php: -------------------------------------------------------------------------------- 1 | index('index')->type('type')->create([ 40 | 'key' => 'value', 41 | ]); 42 | 43 | self::assertObjectHasAttribute('_id', $result); 44 | 45 | return $result; 46 | } 47 | 48 | /** 49 | * @depends testCreate 50 | * 51 | * @param object $result 52 | * 53 | * @return object 54 | */ 55 | public function testUpdate(object $result) 56 | { 57 | $updateResult = static::$build->index('index')->type('type')->update($result->_id, ['key' => 'new value']); 58 | $this->assertTrue($updateResult); 59 | 60 | return $result; 61 | } 62 | 63 | /** 64 | * @depends testUpdate 65 | * 66 | * @param object $result 67 | * 68 | * @return void 69 | */ 70 | public function testDelete(object $result) 71 | { 72 | $deleteResult = static::$build->index('index')->type('type')->delete($result->_id); 73 | $this->assertTrue($deleteResult); 74 | } 75 | 76 | public function testGet() 77 | { 78 | $create = static::$build->index('index1')->create([ 79 | 'key' => 'value', 80 | ]); 81 | 82 | // count 83 | $count = static::$build->index('index1')->where('key', 'value')->count(); 84 | $this->assertTrue($count > 0); 85 | 86 | // get 87 | $result = static::$build->index('index1')->where('key', 'value')->get(); 88 | $this->assertTrue($result->count() > 0); 89 | $this->assertObjectHasAttribute('_id', $result->first()); 90 | $this->assertObjectHasAttribute('_score', $result->first()); 91 | $this->assertObjectHasAttribute('key', $result->first()); 92 | 93 | $one = static::$build->index('index1')->where('key', 'vu')->first(); 94 | $this->assertTrue(is_null($one)); 95 | 96 | static::$build->enableQueryLog(); 97 | $oneExists = static::$build->index('index1')->where('key', 'value')->first(); 98 | $this->assertTrue(!is_null($oneExists)); 99 | $this->assertObjectHasAttribute('_id', $oneExists); 100 | $this->assertObjectHasAttribute('_score', $oneExists); 101 | $this->assertObjectHasAttribute('key', $oneExists); 102 | } 103 | 104 | public function testChunk() 105 | { 106 | $index = uniqid(); 107 | for ($i = 0; $i <= 10; $i++) { 108 | $result = static::$build->index($index)->create(['value' => $i]); 109 | $this->assertTrue(!is_null($result)); 110 | } 111 | 112 | static::$build->index($index)->whereIn('value', [1, 3, 5, 7, 9])->chunk(function ($result) { 113 | foreach ($result as $value) { 114 | $this->assertTrue(in_array($value->value, [1, 3, 5, 7, 9])); 115 | } 116 | }, 2); 117 | } 118 | 119 | public function testPaginate() 120 | { 121 | $index = uniqid(); 122 | for ($i = 0; $i <= 100; $i++) { 123 | $result = static::$build->index($index)->create(['value' => $i]); 124 | $this->assertTrue(!is_null($result)); 125 | } 126 | sleep(2); 127 | $result = static::$build->index($index)->whereIn('value', [1, 3, 5, 7, 9])->paginate(1, 2); 128 | $this->assertTrue(!empty($result->get('data'))); 129 | } 130 | } 131 | --------------------------------------------------------------------------------