├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── composer.lock └── src ├── Builder.php └── Grammar.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 俞俊杰 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 | # elasticsearch-query-builder 2 | 3 | ## 安装 4 | 5 | ``` 6 | composer require jfxy/elasticsearch-query-builder 7 | ``` 8 | 9 | ## 注意 10 | * elasticsearch <= 6.8 11 | * php >= 7.1 12 | * 需要子类继承**Jfxy\ElasticsearchQuery\Builder**并实现**query()** 和 **scrollQuery()** 13 | * 复杂的业务查询应该在子类中封装 14 | * 下面将子类定义为**Es** 15 | 16 | ## 方法 17 | #### select 18 | ```php 19 | public function select($fields) :self 20 | 21 | ->select('id','name') 22 | ->select(['id','name']) 23 | ``` 24 | 25 | #### where 26 | * 比较运算符支持 **=,>,>=,<,<=,!=,<>** 27 | * where、orWhere、whereNot、orWhereNot均支持闭包调用,而orWhere、whereNot、orWhereNot则是对闭包内的整体条件进行 or 和 not 的操作,闭包用法类似mysql中对闭包内的条件前后加上() 28 | * 在封装业务代码存在or关系时,应使用闭包包裹内部条件 29 | ```php 30 | public function where($field, $operator = null, $value = null, $boolean = 'and', $not = false, $filter = false) :self 31 | public function orWhere($field, $operator = null, $value = null) :self 32 | public function whereNot($field, $value = null) :self 33 | public function orWhereNot($field, $value = null) :self 34 | 35 | ->where('id',1) 36 | ->where('id','=',1) 37 | ->where('id',[1,2]) // 等同于 ->whereIn('id',[1,2]) 38 | ->where('news_postdate','<=','2020-09-01') // 等同于 ->whereBetween('news_postdate',['<=' => '2020-09-01']) 39 | 40 | // 闭包用法 41 | ->where(function($query){ 42 | return $query->where('id',1)->orWhere('status','>',0); 43 | }) 44 | ->orWhere(function($query){ 45 | return $query->where('id',1)->orWhere('status','>',0); 46 | }) 47 | 48 | // 数组用法,下面两种写法类似,数组用法下的time条件顺序跟直接传入where方法顺序一致即可 49 | ->where(['id' => 1,'status' => [0,1],['time','>=','2020-09-01']]) 50 | ->where(function($query){ 51 | $query->where('id',1)->where('status',[0,1])->where('time','>=','2020-09-01'); 52 | }) 53 | 54 | // whereNot实现 a != 1 and b != 2 55 | ->whereNot('a',1)->whereNot('b',2) 56 | 57 | // whereNot实现 a != 1 or b != 2,即not(a=1 and b=2) 58 | ->whereNot(['a'=>1,'b'=>2]) 59 | ->whereNot(function($query){ 60 | $query->where('a',1)->where('b',2); 61 | }) 62 | ``` 63 | 64 | #### filter 65 | * 用法同where一致,不过条件会写在filter下 66 | ```php 67 | public function filter($field, $operator = null, $value = null, $boolean = 'and',$not = false) :self 68 | public function orFilter($field, $operator = null, $value = null) :self 69 | public function filterNot($field, $value = null) :self 70 | public function orFilterNot($field, $value = null) :self 71 | ``` 72 | 73 | #### in 74 | ```php 75 | public function whereIn($field, array $value, $boolean = 'and', $not = false) :self 76 | public function whereNotIn($field, array $value, $boolean = 'and') :self 77 | public function orWhereIn($field, array $value) :self 78 | public function orWhereNotIn($field, array $value) :self 79 | 80 | ->whereIn('id',[1,2]) 81 | ``` 82 | 83 | #### between 84 | * 默认为闭区间,比较运算符支持 **>,>=,<,<=** 85 | ```php 86 | public function whereBetween($field, array $value, $boolean = 'and', $not = false) :self 87 | public function whereNotBetween($field, array $value, $boolean = 'and') :self 88 | public function orWhereBetween($field, array $value) :self 89 | public function orWhereNotBetween($field, array $value) :self 90 | 91 | ->whereBetween('id',[1,10]) // 1 <= id <= 10 92 | ->whereBetween('id',[1,'<' => 10]) // 1 <= id < 10 93 | ->whereBetween('id',['>=' => 1,'<' => 10]) // 1 <= id < 10 94 | ``` 95 | 96 | #### exists 97 | * 字段不存在或为null 98 | ```php 99 | public function whereExists($field,$boolean = 'and', $not = false) :self 100 | public function whereNotExists($field) :self 101 | public function orWhereExists($field) :self 102 | public function orWhereNotExists($field) :self 103 | 104 | ->whereExists('news_uuid') 105 | ``` 106 | 107 | #### prefix 前缀匹配 108 | ```php 109 | public function wherePrefix($field, $value, $appendParams = [], $boolean = 'and', $not = false) :self 110 | public function whereNotPrefix($field, $value, $appendParams = []) :self 111 | public function orWherePrefix($field, $value, $appendParams = []) :self 112 | public function orWhereNotPrefix($field, $value, $appendParams = []) :self 113 | 114 | ->wherePrefix('news_url','http://www.baidu.com') 115 | ``` 116 | 117 | #### wildcard 通配符匹配 118 | ```php 119 | public function whereWildcard($field, $value, $appendParams = [], $boolean = 'and', $not = false) :self 120 | public function whereNotWildcard($field, $value, $appendParams = []) :self 121 | public function orWhereWildcard($field, $value, $appendParams = []) :self 122 | public function orWhereNotWildcard($field, $value, $appendParams = []) :self 123 | 124 | ->whereWildcard('media_name','*公安') 125 | ``` 126 | 127 | #### regexp 正则匹配 128 | ```php 129 | public function whereRegexp($field, $value, $appendParams = [], $boolean = 'and', $not = false) :self 130 | public function whereNotRegexp($field, $value, $appendParams = []) :self 131 | public function orWhereRegexp($field, $value, $appendParams = []) :self 132 | public function orWhereNotRegexp($field, $value, $appendParams = []) :self 133 | 134 | ->whereRegexp('media_name','.*公安') 135 | ``` 136 | 137 | #### fuzzy 模糊查询 138 | ```php 139 | public function whereFuzzy($field, $value, $appendParams = [], $boolean = 'and', $not = false) :self 140 | public function whereNotFuzzy($field, $value, $appendParams = []) :self 141 | public function orWhereFuzzy($field, $value, $appendParams = []) :self 142 | public function orWhereNotFuzzy($field, $value, $appendParams = []) :self 143 | 144 | ->whereFuzzy('news_title','安徽合肥') 145 | ``` 146 | 147 | #### whereRaw 原生条件 148 | ```php 149 | public function whereRaw($where, $boolean = 'and', $not = false) :self 150 | public function orWhereRaw($where) :self 151 | 152 | // 下面的例子是由于where方法提供的term查询无法设置一些其他的参数,可以改为使用whereRaw 153 | ->whereRaw([ 154 | "term" => [ 155 | "news_title" => [ 156 | "value" => "安徽", 157 | "boost" => 2 158 | ] 159 | ] 160 | ]) 161 | 162 | ->whereRaw([ 163 | 'bool' => [ 164 | 'must' => [ 165 | "term" => [ 166 | "news_title" => [ 167 | "value" => "安徽", 168 | "boost" => 2 169 | ] 170 | ] 171 | ] 172 | ] 173 | ]) 174 | ``` 175 | 176 | #### match 177 | * whereMatch方法,$type=match、match_phrase、match_phrase_prefix 178 | * whereMultiMatch方法,$type=best_fields、most_fields、cross_fields、phrase、phrase_prefix 179 | ```php 180 | // 单字段 181 | public function whereMatch($field, $value = null,$type = 'match',array $appendParams = [], $boolean = 'and', $not = false) :self 182 | public function orWhereMatch($field, $value = null,$type = 'match',array $appendParams = []) :self 183 | public function whereNotMatch($field, $value = null,$type = 'match',array $appendParams = []) :self 184 | public function orWhereNotMatch($field, $value = null,$type = 'match',array $appendParams = []) :self 185 | // 多字段 186 | public function whereMultiMatch($field, $value = null,$type = 'best_fields',array $appendParams = [], $boolean = 'and', $not = false) :self 187 | public function orWhereMultiMatch($field, $value = null,$type = 'best_fields',array $appendParams = []) :self 188 | public function whereNotMultiMatch($field, $value = null,$type = 'best_fields',array $appendParams = []) :self 189 | public function orWhereNotMultiMatch($field, $value = null,$type = 'best_fields',array $appendParams = []) :self 190 | 191 | ->whereMatch('news_title','上海','match_phrase',['slop'=>1]) 192 | ->whereMultiMatch(['news_title','news_content'],'上海','phrase',["operator" => "OR"]) 193 | ``` 194 | 195 | #### minimumShouldMatch 最小匹配度 196 | ```php 197 | public function minimumShouldMatch($value) :self 198 | 199 | ->where('aa',1)->orWhere('bb',1)->orWhere('cc',1)->minimumShouldMatch(2) 200 | 201 | ->where(function(Es $query){ 202 | $query->where('aa',1)->orWhere('bb',1)->orWhere('cc',1) 203 | ->minimumShouldMatch('50%'); 204 | }) 205 | 206 | ->postWhere(function(Es $query){ 207 | $query->where('aa',1)->orWhere('bb',1)->orWhere('cc',1) 208 | ->minimumShouldMatch('50%'); 209 | }) 210 | ``` 211 | 212 | #### whereNested nested类型字段查询 213 | * 仅支持传入闭包和数组条件 214 | ```php 215 | public function whereNested($path,$wheres,$appendParams = []) :self 216 | 217 | ->whereNested('skus',function(Es $query){ 218 | $query->where('skus.title','iphone')->where('skus.des','iphone'); 219 | },['inner_hits'=>['highlight' => ['fields'=>['skus.title'=>new \stdClass()]]]]); 220 | 221 | ->whereNested('skus',['skus.title' => 'iphone','skus.description' => 'iphone',['skus.price','>','100']],['inner_hits'=>['highlight' => ['fields'=>['skus.title'=>new \stdClass()]]]]); 222 | ``` 223 | 224 | #### postWhere 后置过滤器 225 | * postWhere方法添加的条件会作用于post_filter查询,条件作用于聚合之后 226 | * postWhere方法参数同where方法相同,复杂的检索可以传入数组或闭包 227 | ```php 228 | public function postWhere($field, $operator = null, $value = null, $boolean = 'and',$not = false) :self 229 | 230 | ->postWhere('platform','wx') 231 | ->postWhere(['platform' => ['wx','web'],['news_posttime','>','2020-09-01 00:00:00']]) 232 | ->postWhere(function(Es $query){ 233 | $query->where('platform','wx')->whereNotMatch('news_title','安徽合肥')->orWhereIn('news_postdate',['2020-09-01','2020-09-02']); 234 | }) 235 | ``` 236 | 237 | #### when 238 | * $value为true时会执行$callback,否则当$default存在时会执行$default 239 | ```php 240 | public function when($value,$callback,$default = null) :self 241 | 242 | ->when(1 > 2,function($query){ 243 | return $query->whereBetween('news_postdate',['2020-05-01','2020-05-05']); 244 | },function($query){ 245 | return $query->whereBetween('news_postdate',['2020-05-09','2020-05-10']); 246 | }) 247 | ``` 248 | 249 | #### collapse 折叠 250 | * 使用collapse方法并不会使返回的总数发生变化,计算折叠后的总数需要配合cardinality聚合使用 251 | * collapse方法和paginator方法一起使用时,paginator方法内部会对折叠的字段做cardinality聚合,不需要考虑collapse的总数问题 252 | ```php 253 | public function collapse(string $field,array $appendParams = []) :self 254 | 255 | ->collapse('news_sim_hash') 256 | ->collapse('news_sim_hash')->aggs('alias','cardinality',['field'=>'news_sim_hash']) 257 | ->collapse('news_sim_hash')->cardinality('news_sim_hash') 258 | ->collapse('news_sim_hash')->paginator() 259 | ``` 260 | 261 | #### from 262 | ```php 263 | public function from(int $value) :self 264 | ``` 265 | #### size 266 | ```php 267 | public function size(int $value) :self 268 | ``` 269 | #### orderBy 排序 270 | ```php 271 | public function orderBy(string $field, $sort = 'asc') :self 272 | 273 | ->orderBy('news_posttime','asc')->orderBy('news_like_count','desc') 274 | ``` 275 | #### highlight 高亮 276 | * 高亮配置及高亮字段 277 | * 建议先在Es子类中设置highlightConfig通用属性 278 | ```php 279 | // 根据自己的需要在子类中配置 280 | public $highlightConfig = [ 281 | "require_field_match" => false, // 是否只高亮查询的字段 282 | "number_of_fragments" => 0, // 高亮字段会被分段,返回分段的个数,设置0不分段 283 | "pre_tags" => "", 284 | "post_tags" => "", 285 | ]; 286 | ``` 287 | * 使用highlightConfig方法会覆盖highlightConfig通用属性中的同键名配置 288 | * highlight方法指定高亮字段并且设置指定字段的高亮属性 289 | ```php 290 | public function highlight(string $field,array $params = []) 291 | public function highlightConfig(array $config = []) 292 | 293 | ->highlightConfig(['require_field_match'=>false,'number_of_fragments' => 0,'pre_tags'=>'

','post_tags'=>'

']) 294 | ->highlight('news_title')->highlight('news_digest',['number_of_fragments' => 0]) 295 | ``` 296 | 297 | #### aggs 聚合 298 | * $alias参数是该聚合的别名 299 | * $type参数是聚合的类型,terms、histogram、date_histogram、date_range、range、cardinality、avg、sum、min、max、extended_stats、top_hits、filter... 300 | * $params参数是不同聚合类型下的条件键值对数组 301 | * ...$subGroups参数是嵌套聚合,通过传递闭包参数调用,可同时传递多个闭包 302 | ```php 303 | public function aggs(string $alias,string $type = 'terms',$params = [], ... $subGroups) :self 304 | 305 | ->aggs('alias','terms',['field'=>'platform','size'=>15,'order' => ['_count'=>'asc']]) 306 | ->aggs('alias','date_histogram',['field'=>'news_posttime','interval' => 'day','format' => 'yyyy-MM-dd','min_doc_count' => 0]) 307 | ->aggs('alias','histogram',['field'=>'news_like_count','interval'=>10]) 308 | ->aggs('alias','extended_stats',['field'=>'news_like_count']) 309 | ->aggs('alias','cardinality',['field'=>'news_sim_hash']) 310 | ->aggs('alias','avg',['field'=>'news_like_count']) 311 | ->aggs('alias','sum',['field'=>'news_like_count']) 312 | ->aggs('alias','min',['field'=>'news_like_count']) 313 | ->aggs('alias','max',['field'=>'news_like_count']) 314 | ->aggs('alias','date_range',[ 315 | 'field' => 'news_posttime', 316 | 'format'=> 'yyyy-MM-dd', 317 | 'ranges'=>[ 318 | ['from'=>'2020-09-01','to'=>'2020-09-02'], 319 | ['from'=>"2020-09-02",'to'=>'2020-09-03'] 320 | ] 321 | ]) 322 | ->aggs('alias','range',[ 323 | 'field' => 'media_CI', 324 | 'ranges'=>[ 325 | ['key'=>'0-500','to'=>'500'], 326 | ['key'=>'500-1000','from'=>'500','to'=>'1000'], 327 | ['key'=>'1000-∞','from'=>'1000'], 328 | ] 329 | ]) 330 | ->aggs('alias','top_hits',['size'=>1]) 331 | ->aggs('alias','filter',['term' => ['platform' => 'web']]) 332 | ``` 333 | 334 | * groupBy方法是aggs的terms类型聚合的封装 335 | ````php 336 | public function groupBy(string $field, array $appendParams = [], ... $subGroups) :self 337 | 338 | ->groupBy('platform',['size'=>20,'order'=>['_count'=>'asc']]) 339 | 340 | // $appendParams 常用的一些设置,不同的聚合类型参数不同 341 | $appendParams = [ 342 | 'size' => 10, // 默认 343 | 'order' => ['_count'=>'desc'] // 默认,文档数量倒序 344 | 'order' => ['_count'=>'asc'] // 文档数量顺序 345 | 'order' => ['_key'=>'desc'] // 分组key倒序 346 | 'order' => ['_key'=>'asc'] // 分组key顺序 347 | ... 348 | ] 349 | ```` 350 | 351 | * dateGroupBy方法是aggs的date_histogram类型聚合的封装 352 | ````php 353 | public function dateGroupBy(string $field,string $interval = 'day',string $format = "yyyy-MM-dd",array $appendParams = [], ... $subGroups) :self 354 | 355 | ->dateGroupBy('news_posttime','day','yyyy-MM-dd') 356 | ```` 357 | 358 | * cardinality方法是aggs的cardinality类型聚合的封装 359 | ````php 360 | public function cardinality(string $field,array $appendParams = []) :self 361 | 362 | ->cardinality('news_sim_hash') 363 | ```` 364 | 365 | * avg方法是aggs的avg类型聚合的封装 366 | ````php 367 | public function avg(string $field,array $appendParams = []) :self 368 | 369 | ->avg('media_CI') 370 | ```` 371 | 372 | * sum方法是aggs的sum类型聚合的封装 373 | ````php 374 | public function sum(string $field,array $appendParams = []) :self 375 | 376 | ->sum('media_CI') 377 | ```` 378 | 379 | * min方法是aggs的min类型聚合的封装 380 | ````php 381 | public function min(string $field,array $appendParams = []) :self 382 | 383 | ->min('media_CI') 384 | ```` 385 | 386 | * max方法是aggs的max类型聚合的封装 387 | ````php 388 | public function max(string $field,array $appendParams = []) :self 389 | 390 | ->max('media_CI') 391 | ```` 392 | 393 | * stats方法是aggs的stats类型聚合的封装 394 | ````php 395 | public function stats(string $field,array $appendParams = []) :self 396 | 397 | ->stats('media_CI') 398 | ```` 399 | 400 | * extendedStats方法是aggs的extended_stats类型聚合的封装 401 | ````php 402 | public function extendedStats(string $field,array $appendParams = []) :self 403 | 404 | ->extendedStats('media_CI') 405 | ```` 406 | 407 | * topHits方法是top_hits类型聚合的封装 408 | ```php 409 | public function topHits($params) :self 410 | 411 | ->topHits([ 412 | 'from' => 2, 413 | 'size' => 1, 414 | 'sort' => ['news_posttime' => ['order' => 'asc']], 415 | '_source' => ['news_title','news_posttime','news_url','news_digest'], 416 | 'highlight' => [ 417 | 'require_field_match'=>true, 418 | 'pre_tags'=>'

', 419 | 'post_tags'=>'

', 420 | 'fields' => [ 421 | 'news_title' => new \stdClass(), 422 | 'news_digest' => ['number_of_fragments' => 0]] 423 | ] 424 | ]); 425 | 426 | ->topHits(function(Es $query){ 427 | $query->size(1)->from(2) 428 | ->orderBy('news_posttime','asc') 429 | ->select(['news_title','news_posttime','news_url','news_digest']) 430 | ->highlight('news_title') 431 | ->highlight('news_digest',['number_of_fragments' => 0]); 432 | }) 433 | ``` 434 | 435 | * aggsFilter方法是filter类型聚合的封装,可在聚合内部进行条件过滤,$wheres参数仅支持数组和闭包,可参考where方法 436 | ```php 437 | public function aggsFilter($alias,$wheres,... $subGroups) :self 438 | 439 | ->aggsFilter('alias1',function(Es $query){ 440 | $query->where('platform','web'); 441 | },function(Es $query){ 442 | $query->groupBy('platform_name',['size'=>30]); 443 | }) 444 | ->aggsFilter('alias2',['platform'=>'web','news_title'=>'合肥',['news_postdate','>=','2020-09-01']],function(Es $query){ 445 | $query->groupBy('platform_name',['size'=>30]); 446 | }) 447 | ``` 448 | 449 | #### raw 450 | * 原生dsl语句查询,不支持添加其他条件 451 | ```php 452 | public function raw($dsl) :self 453 | 454 | ->raw(['query'=>['match_all' => new \stdClass()]])->get() 455 | ->raw(json_encode(['query'=>['match_all' => new \stdClass()]]))->get() 456 | ``` 457 | 458 | #### dsl 459 | * 返回待查询的dsl语句,$type = 'json',返回json字符串 460 | ```php 461 | public function dsl($type = 'array') 462 | ``` 463 | 464 | #### get 465 | * 查询结果,$directReturn = true,返回未经处理的结果 466 | ```php 467 | public function get($directReturn = false) 468 | 469 | // $directReturn = false时,返回以下数据 470 | [ 471 | 'total' => 文档总数, 472 | 'list' => 文档列表, 473 | 'aggs' => 聚合结果(存在聚合时返回), 474 | 'scroll_id' => scroll_id(游标查询时返回) 475 | ] 476 | ``` 477 | 478 | #### paginator 分页 479 | * paginator方法和collapse方法一起使用时,paginator方法内部会对折叠的字段做cardinality聚合,不需要考虑collapse的总数问题 480 | ```php 481 | public function paginator(int $page = 1, int $size = 10) 482 | 483 | ->collapse('news_sim_hash')->paginator() 484 | 485 | [ 486 | 'total' => 文档总数(存在collapse时,是计算折叠后的总数), 487 | 'original_total' => 文档总数(不受collapse影响), 488 | 'per_page' => 每页条数, 489 | 'current_page' => 当前页码, 490 | 'last_page' => 最大页码, 491 | 'list' => 文档列表, 492 | 'aggs' => 聚合结果(存在聚合时返回) 493 | ] 494 | ``` 495 | 496 | #### first 497 | * 返回第一条记录,$directReturn = true,返回未经处理的结果 498 | ```php 499 | public function first($directReturn = false) 500 | ``` 501 | 502 | #### count 计数 503 | ```php 504 | public function count() 505 | ``` 506 | 507 | #### scroll 游标 508 | ```php 509 | $data = Es::init()->scroll()->size(1000)->where('platform','app')->get(); 510 | $es = Es::init(); 511 | while(true){ 512 | $data = $es->scrollId($data['scroll_id'])->get(); 513 | // do something 514 | ... 515 | } 516 | ``` 517 | 518 | ## 封装示例 519 | ```php 520 | // 本例实现的是多个关键词组短语匹配,词组之间是or关系,词组内为and关系 521 | $keywordGroups = [ 522 | ['中国','上海'], 523 | ['安徽','合肥'], 524 | ]; 525 | public function keywords($keywordGroups,$type = 'full'){ 526 | $this->where(function(self $query)use($keywordGroups,$type){ 527 | foreach($keywordGroups as $keywordGroup){ 528 | $query->orWhere(function(self $query1)use($keywordGroup,$type){ 529 | foreach($keywordGroup as $keyword){ 530 | if('full' == $type){ 531 | $query1->whereMultiMatch(['news_title','news_content'],$keyword,'phrase',["operator" => "OR"]); 532 | }elseif('title' == $type){ 533 | $query1->whereMatch('news_title',$keyword,'match_phrase'); 534 | }elseif('content' == $type){ 535 | $query1->whereMatch('news_content',$keyword,'match_phrase'); 536 | } 537 | } 538 | }); 539 | } 540 | }); 541 | return $this; 542 | } 543 | 544 | // 本例实现的是排除关键词组内的关键词 545 | $keywords = ['美国','日本']; 546 | public function keywordsExclude($keywords){ 547 | $this->where(function(self $query)use($keywords){ 548 | foreach($keywords as $keyword){ 549 | $query->whereNotMultiMatch(['news_title','news_content'],$keyword,'phrase',["operator" => "OR"]); 550 | } 551 | }); 552 | return $this; 553 | } 554 | ``` 555 | ## query、scrollQuery实现示例 556 | ```php 557 | public function query() 558 | { 559 | if(!is_string($this->dsl)){ 560 | $this->dsl = json_encode($this->dsl,JSON_UNESCAPED_UNICODE); 561 | } 562 | 563 | /****用内部组装好的$this->dsl进行查询,并返回es的响应...****/ 564 | 565 | return $response; 566 | } 567 | ``` 568 | 569 | ## 调用示例 570 | ```php 571 | Es::init()->select('id','name')->where('id',3)->dsl(); 572 | Es::init()->select('id','name')->where('id',3)->groupBy('platform_name')->get(); 573 | Es::init()->select('id','name')->where('id',3)->paginator(2,15); 574 | Es::init()->select('id','name')->where('id',3)->first(); 575 | Es::init()->select('id','name')->where('id',3)->count(); 576 | 577 | Es::init()->select('news_title','news_url','news_uuid','platform') 578 | ->where('platform',['wx','web','app']) 579 | ->whereBetween('news_postdate',['2020-09-01','2020-09-10']) 580 | ->keywords([['中国','上海'],['安徽','合肥']],'full') 581 | ->keywordsExclude(['美国','日本']) 582 | ->highlight('news_title') 583 | ->groupBy('platform',['size'=>20,'order'=>['_count'=>'asc']],function(Es $query){ 584 | $query->groupBy('platform_name',['size'=>30]); 585 | },function(Es $query){ 586 | $query->groupBy('platform_domian_pri',['size'=>30],function(Es $query){ 587 | $query->topHits(['size'=>1]); 588 | }); 589 | }) 590 | ->dateGroupBy('news_posttime') 591 | ->aggs('news_like_count','histogram',['interval'=>100]) 592 | ->cardinality('news_sim_hash') 593 | ->avg('media_CI') 594 | ->sum('media_CI') 595 | ->max('media_CI') 596 | ->min('media_CI') 597 | ->extendedStats('media_CI') 598 | ->get(); 599 | ``` 600 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jfxy/elasticsearch-query-builder", 3 | "description": "a query builder package for elasticsearch", 4 | "type": "library", 5 | "keywords": [ 6 | "elasticsearch","php" 7 | ], 8 | "require": { 9 | "php": "^7.1" 10 | }, 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "yjj", 15 | "email": "651069242@qq.com" 16 | } 17 | ], 18 | "autoload": { 19 | "psr-4": { 20 | "Jfxy\\ElasticsearchQuery\\": "src/" 21 | } 22 | }, 23 | "minimum-stability": "stable", 24 | "prefer-stable": true 25 | } 26 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "9cf335fca5f807bb07b9bdf8d0eae071", 8 | "packages": [], 9 | "packages-dev": [], 10 | "aliases": [], 11 | "minimum-stability": "dev", 12 | "stability-flags": [], 13 | "prefer-stable": true, 14 | "prefer-lowest": false, 15 | "platform": { 16 | "php": "^7.1" 17 | }, 18 | "platform-dev": [] 19 | } 20 | -------------------------------------------------------------------------------- /src/Builder.php: -------------------------------------------------------------------------------- 1 | 'eq', 48 | '>' => 'gt', 49 | '>=' => 'gte', 50 | '<' => 'lt', 51 | '<=' => 'lte', 52 | ]; 53 | 54 | protected $operators = [ 55 | '=', '>', '<', '>=', '<=', '!=', '<>' 56 | ]; 57 | 58 | public function __construct() 59 | { 60 | $this->grammar = Grammar::getInstance(); 61 | } 62 | 63 | public static function init() 64 | { 65 | return new static(); 66 | } 67 | 68 | /** 69 | * @param $fields 70 | * @return $this 71 | */ 72 | public function select($fields): self 73 | { 74 | $this->fields = is_array($fields) ? $fields : func_get_args(); 75 | return $this; 76 | } 77 | 78 | /** 79 | * @param $field 80 | * @param null $operator 81 | * @param null $value 82 | * @param string $boolean 83 | * @param bool $not 84 | * @param bool $filter 85 | * @return $this 86 | * @throws Exception 87 | */ 88 | public function where($field, $operator = null, $value = null, $boolean = 'and', $not = false, $filter = false): self 89 | { 90 | if (is_array($field)) { 91 | return $this->addArrayOfWheres($field, $boolean, $not, $filter); 92 | } 93 | if ($field instanceof Closure && is_null($operator)) { 94 | return $this->nestedQuery($field, $boolean, $not, $filter); 95 | } 96 | 97 | [$value, $operator] = $this->prepareValueAndOperator( 98 | $value, $operator, func_num_args() === 2 99 | ); 100 | 101 | if (in_array($operator, ['!=', '<>'])) { 102 | $not = !$not; 103 | } 104 | 105 | if (is_array($value)) { 106 | return $this->whereIn($field, $value, $boolean, $not, $filter); 107 | } 108 | 109 | if (in_array($operator, ['>', '<', '>=', '<='])) { 110 | $value = [$operator => $value]; 111 | return $this->whereBetween($field, $value, $boolean, $not, $filter); 112 | } 113 | 114 | $type = 'basic'; 115 | 116 | $this->wheres[] = compact( 117 | 'type', 'field', 'operator', 'value', 'boolean', 'not', 'filter' 118 | ); 119 | return $this; 120 | } 121 | 122 | /** 123 | * @param $field 124 | * @param null $operator 125 | * @param null $value 126 | * @return $this 127 | * @throws Exception 128 | */ 129 | public function orWhere($field, $operator = null, $value = null): self 130 | { 131 | [$value, $operator] = $this->prepareValueAndOperator( 132 | $value, $operator, func_num_args() === 2 133 | ); 134 | return $this->where($field, $operator, $value, 'or'); 135 | } 136 | 137 | 138 | /** 139 | * @param $field 140 | * @param null $value 141 | * @return $this 142 | * @throws Exception 143 | */ 144 | public function whereNot($field, $operator = null, $value = null): self 145 | { 146 | [$value, $operator] = $this->prepareValueAndOperator( 147 | $value, $operator, func_num_args() === 2 148 | ); 149 | return $this->where($field, $operator, $value, 'and', true); 150 | } 151 | 152 | /** 153 | * @param $field 154 | * @param null $value 155 | * @return $this 156 | * @throws Exception 157 | */ 158 | public function orWhereNot($field, $operator = null, $value = null): self 159 | { 160 | [$value, $operator] = $this->prepareValueAndOperator( 161 | $value, $operator, func_num_args() === 2 162 | ); 163 | return $this->where($field, $operator, $value, 'or', true); 164 | } 165 | 166 | /** 167 | * @param $field 168 | * @param null $operator 169 | * @param null $value 170 | * @param string $boolean 171 | * @param bool $not 172 | * @return $this 173 | */ 174 | public function filter($field, $operator = null, $value = null, $boolean = 'and', $not = false): self 175 | { 176 | [$value, $operator] = $this->prepareValueAndOperator( 177 | $value, $operator, func_num_args() === 2 178 | ); 179 | return $this->where($field, $operator, $value, $boolean, $not, true); 180 | } 181 | 182 | /** 183 | * @param $field 184 | * @param null $operator 185 | * @param null $value 186 | * @return $this 187 | */ 188 | public function orFilter($field, $operator = null, $value = null): self 189 | { 190 | [$value, $operator] = $this->prepareValueAndOperator( 191 | $value, $operator, func_num_args() === 2 192 | ); 193 | return $this->filter($field, $operator, $value, 'or'); 194 | } 195 | 196 | /** 197 | * @param $field 198 | * @param null $operator 199 | * @param null $value 200 | * @return $this 201 | */ 202 | public function filterNot($field, $operator = null, $value = null): self 203 | { 204 | [$value, $operator] = $this->prepareValueAndOperator( 205 | $value, $operator, func_num_args() === 2 206 | ); 207 | return $this->filter($field, $operator, $value, 'and', true); 208 | } 209 | 210 | /** 211 | * @param $field 212 | * @param null $operator 213 | * @param null $value 214 | * @return $this 215 | */ 216 | public function orFilterNot($field, $operator = null, $value = null): self 217 | { 218 | [$value, $operator] = $this->prepareValueAndOperator( 219 | $value, $operator, func_num_args() === 2 220 | ); 221 | return $this->filter($field, $operator, $value, 'or', true); 222 | } 223 | 224 | /** 225 | * 单字段查询 226 | * @param $field 227 | * @param null $value 228 | * @param string $type match|match_phrase|match_phrase_prefix 229 | * @param array $appendParams 230 | * @param string $boolean 231 | * @param bool $not 232 | * @return $this 233 | */ 234 | public function whereMatch($field, $value = null, $type = 'match', array $appendParams = [], $boolean = 'and', $not = false, $filter = false): self 235 | { 236 | $this->wheres[] = compact( 237 | 'type', 'field', 'value', 'appendParams', 'boolean', 'not', 'filter' 238 | ); 239 | return $this; 240 | } 241 | 242 | /** 243 | * @param $field 244 | * @param null $value 245 | * @param string $type 246 | * @param array $appendParams 247 | * @return $this 248 | */ 249 | public function orWhereMatch($field, $value = null, $type = 'match', array $appendParams = [], $filter = false): self 250 | { 251 | return $this->whereMatch($field, $value, $type, $appendParams, 'or', false, $filter); 252 | } 253 | 254 | /** 255 | * @param $field 256 | * @param null $value 257 | * @param string $type 258 | * @param array $appendParams 259 | * @return $this 260 | */ 261 | public function whereNotMatch($field, $value = null, $type = 'match', array $appendParams = [], $filter = false): self 262 | { 263 | return $this->whereMatch($field, $value, $type, $appendParams, 'and', true, $filter); 264 | } 265 | 266 | /** 267 | * @param $field 268 | * @param null $value 269 | * @param string $type 270 | * @param array $appendParams 271 | * @return $this 272 | */ 273 | public function orWhereNotMatch($field, $value = null, $type = 'match', array $appendParams = [], $filter = false): self 274 | { 275 | return $this->whereMatch($field, $value, $type, $appendParams, 'or', true, $filter); 276 | } 277 | 278 | /** 279 | * 多字段查询 280 | * @param $field 281 | * @param null $value 282 | * @param string $type best_fields|most_fields|cross_fields|phrase|phrase_prefix 283 | * @param array $appendParams 284 | * @param string $boolean 285 | * @param bool $not 286 | * @return $this 287 | */ 288 | public function whereMultiMatch($field, $value = null, $type = 'best_fields', array $appendParams = [], $boolean = 'and', $not = false, $filter = false): self 289 | { 290 | [$type, $matchType] = ['multi_match', $type]; 291 | $this->wheres[] = compact( 292 | 'type', 'field', 'value', 'matchType', 'appendParams', 'boolean', 'not', 'filter' 293 | ); 294 | return $this; 295 | } 296 | 297 | /** 298 | * @param $field 299 | * @param null $value 300 | * @param string $type 301 | * @param array $appendParams 302 | * @return $this 303 | */ 304 | public function orWhereMultiMatch($field, $value = null, $type = 'best_fields', array $appendParams = [], $filter = false): self 305 | { 306 | return $this->whereMultiMatch($field, $value, $type, $appendParams, 'or', false, $filter); 307 | } 308 | 309 | /** 310 | * @param $field 311 | * @param null $value 312 | * @param string $type 313 | * @param array $appendParams 314 | * @return $this 315 | */ 316 | public function whereNotMultiMatch($field, $value = null, $type = 'best_fields', array $appendParams = [], $filter = false): self 317 | { 318 | return $this->whereMultiMatch($field, $value, $type, $appendParams, 'and', true, $filter); 319 | } 320 | 321 | /** 322 | * @param $field 323 | * @param null $value 324 | * @param string $type 325 | * @param array $appendParams 326 | * @return $this 327 | */ 328 | public function orWhereNotMultiMatch($field, $value = null, $type = 'best_fields', array $appendParams = [], $filter = false): self 329 | { 330 | return $this->whereMultiMatch($field, $value, $type, $appendParams, 'or', true, $filter); 331 | } 332 | 333 | /** 334 | * @param $field 335 | * @param array $value 336 | * @param string $boolean 337 | * @param bool $not 338 | * @return $this 339 | */ 340 | public function whereIn($field, array $value, $boolean = 'and', $not = false, $filter = false): self 341 | { 342 | $type = 'in'; 343 | $this->wheres[] = compact('type', 'field', 'value', 'boolean', 'not', 'filter'); 344 | return $this; 345 | } 346 | 347 | /** 348 | * @param $field 349 | * @param array $value 350 | * @return $this 351 | */ 352 | public function whereNotIn($field, array $value, $filter = false): self 353 | { 354 | return $this->whereIn($field, $value, 'and', true, $filter); 355 | } 356 | 357 | /** 358 | * @param $field 359 | * @param array $value 360 | * @return $this 361 | */ 362 | public function orWhereIn($field, array $value, $filter = false): self 363 | { 364 | return $this->whereIn($field, $value, 'or', false, $filter); 365 | } 366 | 367 | /** 368 | * @param $field 369 | * @param array $value 370 | * @return $this 371 | */ 372 | public function orWhereNotIn($field, array $value, $filter = false): self 373 | { 374 | return $this->whereIn($field, $value, 'or', true, $filter); 375 | } 376 | 377 | /** 378 | * @param $field 379 | * @param array $value 380 | * @param string $boolean 381 | * @param bool $not 382 | * @return $this 383 | */ 384 | public function whereBetween($field, array $value, $boolean = 'and', $not = false, $filter = false): self 385 | { 386 | $type = 'between'; 387 | $this->wheres[] = compact('type', 'field', 'value', 'boolean', 'not', 'filter'); 388 | return $this; 389 | } 390 | 391 | /** 392 | * @param $field 393 | * @param array $value 394 | * @return $this 395 | */ 396 | public function whereNotBetween($field, array $value, $filter = false): self 397 | { 398 | return $this->whereBetween($field, $value, 'and', true, $filter); 399 | } 400 | 401 | /** 402 | * @param $field 403 | * @param array $value 404 | * @return $this 405 | */ 406 | public function orWhereBetween($field, array $value, $filter = false): self 407 | { 408 | return $this->whereBetween($field, $value, 'or', false, $filter); 409 | } 410 | 411 | /** 412 | * @param $field 413 | * @param array $value 414 | * @return $this 415 | */ 416 | public function orWhereNotBetween($field, array $value, $filter = false): self 417 | { 418 | return $this->whereBetween($field, $value, 'or', true, $filter); 419 | } 420 | 421 | /** 422 | * @param $field 423 | * @param string $boolean 424 | * @param bool $not 425 | * @return $this 426 | */ 427 | public function whereExists($field, $boolean = 'and', $not = false, $filter = false): self 428 | { 429 | $type = 'exists'; 430 | $this->wheres[] = compact('type', 'field', 'boolean', 'not', 'filter'); 431 | return $this; 432 | } 433 | 434 | /** 435 | * @param $field 436 | * @return $this 437 | */ 438 | public function whereNotExists($field, $filter = false): self 439 | { 440 | return $this->whereExists($field, 'and', true, $filter); 441 | } 442 | 443 | /** 444 | * @param $field 445 | * @return $this 446 | */ 447 | public function orWhereExists($field, $filter = false): self 448 | { 449 | return $this->whereExists($field, 'or', false, $filter); 450 | } 451 | 452 | /** 453 | * @param $field 454 | * @return $this 455 | */ 456 | public function orWhereNotExists($field, $filter = false): self 457 | { 458 | return $this->whereExists($field, 'or', true, $filter); 459 | } 460 | 461 | /** 462 | * @param $field 463 | * @param $value 464 | * @param $appendParams 465 | * @param string $boolean 466 | * @param bool $not 467 | * @return $this 468 | */ 469 | public function wherePrefix($field, $value, $appendParams = [], $boolean = 'and', $not = false, $filter = false): self 470 | { 471 | $type = 'prefix'; 472 | $this->wheres[] = compact('type', 'field', 'value', 'appendParams', 'boolean', 'not', 'filter'); 473 | return $this; 474 | } 475 | 476 | /** 477 | * @param $field 478 | * @param $value 479 | * @param array $appendParams 480 | * @return $this 481 | */ 482 | public function whereNotPrefix($field, $value, $appendParams = [], $filter = false): self 483 | { 484 | return $this->wherePrefix($field, $value, $appendParams, 'and', true, $filter); 485 | } 486 | 487 | /** 488 | * @param $field 489 | * @param $value 490 | * @param array $appendParams 491 | * @return $this 492 | */ 493 | public function orWherePrefix($field, $value, $appendParams = [], $filter = false): self 494 | { 495 | return $this->wherePrefix($field, $value, $appendParams, 'or', false, $filter); 496 | } 497 | 498 | /** 499 | * @param $field 500 | * @param $value 501 | * @param array $appendParams 502 | * @return $this 503 | */ 504 | public function orWhereNotPrefix($field, $value, $appendParams = [], $filter = false): self 505 | { 506 | return $this->wherePrefix($field, $value, $appendParams, 'or', true, $filter); 507 | } 508 | 509 | /** 510 | * @param $field 511 | * @param $value 512 | * @param $appendParams 513 | * @param string $boolean 514 | * @param bool $not 515 | * @return $this 516 | */ 517 | public function whereWildcard($field, $value, $appendParams = [], $boolean = 'and', $not = false, $filter = false): self 518 | { 519 | $type = 'wildcard'; 520 | $this->wheres[] = compact('type', 'field', 'value', 'appendParams', 'boolean', 'not', 'filter'); 521 | return $this; 522 | } 523 | 524 | /** 525 | * @param $field 526 | * @param $value 527 | * @param array $appendParams 528 | * @return $this 529 | */ 530 | public function whereNotWildcard($field, $value, $appendParams = [], $filter = false): self 531 | { 532 | return $this->whereWildcard($field, $value, $appendParams, 'and', true, $filter); 533 | } 534 | 535 | /** 536 | * @param $field 537 | * @param $value 538 | * @param array $appendParams 539 | * @return $this 540 | */ 541 | public function orWhereWildcard($field, $value, $appendParams = [], $filter = false): self 542 | { 543 | return $this->whereWildcard($field, $value, $appendParams, 'or', false, $filter); 544 | } 545 | 546 | /** 547 | * @param $field 548 | * @param $value 549 | * @param array $appendParams 550 | * @return $this 551 | */ 552 | public function orWhereNotWildcard($field, $value, $appendParams = [], $filter = false): self 553 | { 554 | return $this->whereWildcard($field, $value, $appendParams, 'or', true, $filter); 555 | } 556 | 557 | /** 558 | * @param $field 559 | * @param $value 560 | * @param $appendParams 561 | * @param string $boolean 562 | * @param bool $not 563 | * @return $this 564 | */ 565 | public function whereRegexp($field, $value, $appendParams = [], $boolean = 'and', $not = false, $filter = false): self 566 | { 567 | $type = 'regexp'; 568 | $this->wheres[] = compact('type', 'field', 'value', 'appendParams', 'boolean', 'not', 'filter'); 569 | return $this; 570 | } 571 | 572 | /** 573 | * @param $field 574 | * @param $value 575 | * @param array $appendParams 576 | * @return $this 577 | */ 578 | public function whereNotRegexp($field, $value, $appendParams = [], $filter = false): self 579 | { 580 | return $this->whereRegexp($field, $value, $appendParams, 'and', true, $filter); 581 | } 582 | 583 | /** 584 | * @param $field 585 | * @param $value 586 | * @param array $appendParams 587 | * @return $this 588 | */ 589 | public function orWhereRegexp($field, $value, $appendParams = [], $filter = false): self 590 | { 591 | return $this->whereRegexp($field, $value, $appendParams, 'or', false, $filter); 592 | } 593 | 594 | /** 595 | * @param $field 596 | * @param $value 597 | * @param array $appendParams 598 | * @return $this 599 | */ 600 | public function orWhereNotRegexp($field, $value, $appendParams = [], $filter = false): self 601 | { 602 | return $this->whereRegexp($field, $value, $appendParams, 'or', true, $filter); 603 | } 604 | 605 | 606 | /** 607 | * @param $field 608 | * @param $value 609 | * @param $appendParams 610 | * @param string $boolean 611 | * @param bool $not 612 | * @return $this 613 | */ 614 | public function whereFuzzy($field, $value, $appendParams = [], $boolean = 'and', $not = false, $filter = false): self 615 | { 616 | $type = 'fuzzy'; 617 | $this->wheres[] = compact('type', 'field', 'value', 'appendParams', 'boolean', 'not', 'filter'); 618 | return $this; 619 | } 620 | 621 | /** 622 | * @param $field 623 | * @param $value 624 | * @param array $appendParams 625 | * @return $this 626 | */ 627 | public function whereNotFuzzy($field, $value, $appendParams = [], $filter = false): self 628 | { 629 | return $this->whereFuzzy($field, $value, $appendParams, 'and', true, $filter); 630 | } 631 | 632 | /** 633 | * @param $field 634 | * @param $value 635 | * @param array $appendParams 636 | * @return $this 637 | */ 638 | public function orWhereFuzzy($field, $value, $appendParams = [], $filter = false): self 639 | { 640 | return $this->whereFuzzy($field, $value, $appendParams, 'or', false, $filter); 641 | } 642 | 643 | /** 644 | * @param $field 645 | * @param $value 646 | * @param array $appendParams 647 | * @return $this 648 | */ 649 | public function orWhereNotFuzzy($field, $value, $appendParams = [], $filter = false): self 650 | { 651 | return $this->whereFuzzy($field, $value, $appendParams, 'or', true, $filter); 652 | } 653 | 654 | /** 655 | * nested类型字段查询 656 | * @param $path 657 | * @param $wheres 658 | * @param $appendParams 659 | * @return $this 660 | * @throws Exception 661 | */ 662 | public function whereNested($path, $wheres, $appendParams = [], $filter = false): self 663 | { 664 | if (!($wheres instanceof Closure) && !is_array($wheres)) { 665 | throw new Exception('非法参数'); 666 | } 667 | $type = 'nested'; 668 | $boolean = 'and'; 669 | $not = false; 670 | $query = $this->newQuery()->where($wheres); 671 | $this->wheres[] = compact('type', 'path', 'query', 'appendParams', 'boolean', 'not', 'filter'); 672 | return $this; 673 | } 674 | 675 | /** 676 | * @param $where 677 | * @param string $boolean 678 | * @param bool $not 679 | * @param bool $filter 680 | * @return $this 681 | */ 682 | public function whereRaw($where, $boolean = 'and', $not = false, $filter = false): self 683 | { 684 | $type = 'raw'; 685 | $where = is_string($where) ? json_decode($where, true) : $where; 686 | $this->wheres[] = compact('type', 'where', 'boolean', 'not', 'filter'); 687 | return $this; 688 | } 689 | 690 | /** 691 | * @param $where 692 | * @return $this 693 | */ 694 | public function orWhereRaw($where, $filter = false): self 695 | { 696 | return $this->whereRaw($where, 'or', false, $filter); 697 | } 698 | 699 | /** 700 | * 后置过滤器 701 | * @param $field 702 | * @param null $operator 703 | * @param null $value 704 | * @param string $boolean 705 | * @param bool $not 706 | * @return $this 707 | * @throws Exception 708 | */ 709 | public function postWhere($field, $operator = null, $value = null, $boolean = 'and', $not = false, $filter = false): self 710 | { 711 | $query = $this->newQuery()->where(...func_get_args()); 712 | $this->postWheres = is_array($this->postWheres) ? $this->postWheres : []; 713 | array_push($this->postWheres, ...$query->wheres); 714 | return $this; 715 | } 716 | 717 | /** 718 | * @param mixed $value 719 | * @param callable $callback 720 | * @param callable|null $default 721 | * @return mixed|$this 722 | */ 723 | public function when($value, $callback, $default = null): self 724 | { 725 | if ($value) { 726 | return $callback($this, $value) ?: $this; 727 | } elseif ($default) { 728 | return $default($this, $value) ?: $this; 729 | } 730 | return $this; 731 | } 732 | 733 | /** 734 | * @param string $field 735 | * @param array $appendParams 736 | * @return $this 737 | */ 738 | public function collapse(string $field, array $appendParams = []): self 739 | { 740 | if (empty($appendParams)) { 741 | $this->collapse = $field; 742 | } else { 743 | $this->collapse = array_merge(['field' => $field], $appendParams); 744 | } 745 | return $this; 746 | } 747 | 748 | /** 749 | * @param int $value 750 | * @return $this 751 | */ 752 | public function from(int $value): self 753 | { 754 | $this->from = $value; 755 | return $this; 756 | } 757 | 758 | /** 759 | * @param int $value 760 | * @return $this 761 | */ 762 | public function size(int $value): self 763 | { 764 | $this->size = $value; 765 | return $this; 766 | } 767 | 768 | /** 769 | * @param $field 770 | * @param string $sort 771 | * @return $this 772 | */ 773 | public function orderBy($field, $sort = 'asc'): self 774 | { 775 | $this->orders[] = is_array($field) ? $field : [ 776 | $field => ['order' => $sort] 777 | ]; 778 | return $this; 779 | } 780 | 781 | /** 782 | * 高亮字段 783 | * @param string $field 784 | * @param array $params 785 | * [ 786 | * "number_of_fragments" => 0 // 字段片段数 787 | * ... 788 | * ] 789 | * @return $this 790 | */ 791 | public function highlight(string $field, array $params = []): self 792 | { 793 | $this->highlight[$field] = $params; 794 | return $this; 795 | } 796 | 797 | /** 798 | * 高亮配置 799 | * @param array $config 800 | * [ 801 | * "require_field_match" => false, // 是否只高亮查询的字段 802 | * "number_of_fragments" => 1, // 高亮字段会被分段,返回分段的个数,设置0不分段 803 | * "pre_tags" => "", 804 | * "post_tags" => "", 805 | * ... 806 | * ] 807 | * @return $this 808 | */ 809 | public function highlightConfig(array $config = []): self 810 | { 811 | $this->highlightConfig = array_merge($this->highlightConfig, $config); 812 | return $this; 813 | } 814 | 815 | /** 816 | * @param $value 817 | * @return $this 818 | */ 819 | public function minimumShouldMatch($value): self 820 | { 821 | $this->minimumShouldMatch = $value; 822 | return $this; 823 | } 824 | 825 | /** 826 | * @param $value 827 | * @return Builder 828 | */ 829 | public function minScore($value): self 830 | { 831 | $this->minScore = $value; 832 | return $this; 833 | } 834 | 835 | /** 836 | * @param string $scroll 837 | * @return $this 838 | */ 839 | public function scroll($scroll = '2m'): self 840 | { 841 | $this->scroll = $scroll; 842 | return $this; 843 | } 844 | 845 | /** 846 | * @param string $scrollId 847 | * @return $this 848 | */ 849 | public function scrollId(string $scrollId): self 850 | { 851 | if (empty($this->scroll)) { 852 | $this->scroll(); 853 | } 854 | $this->scrollId = $scrollId; 855 | return $this; 856 | } 857 | 858 | /** 859 | * @param string $field 860 | * @param string $type 常用聚合[terms,histogram,date_histogram,date_range,range,cardinality,avg,sum,min,max,extended_stats...] 861 | * @param array $appendParams 聚合需要携带的参数,聚合不同参数不同,部分聚合必须传入,比如date_histogram需传入[interval=>day,hour...] 862 | * @param mixed ...$subGroups 863 | * @return $this 864 | */ 865 | public function aggs(string $alias, string $type = 'terms', $params = [], ...$subGroups): self 866 | { 867 | $aggs = [ 868 | 'type' => $type, 869 | 'alias' => $alias, 870 | 'params' => $params, 871 | ]; 872 | foreach ($subGroups as $subGroup) { 873 | call_user_func($subGroup, $query = $this->newQuery()); 874 | $aggs['subGroups'][] = $query; 875 | } 876 | $this->aggs[] = $aggs; 877 | return $this; 878 | } 879 | 880 | /** 881 | * terms 聚合 882 | * @param string $field 883 | * @param array $appendParams 聚合需要携带的参数 884 | * [ 885 | * 'size' => 10, // 默认 886 | * 'order' => ['_count'=>'desc'] // 默认 887 | * 'order' => ['_count'=>'asc'] 888 | * 'order' => ['_key'=>'desc'] 889 | * 'order' => ['_key'=>'asc'] 890 | * ... 891 | * ] 892 | * @param mixed ...$subGroups 893 | * @return $this 894 | */ 895 | public function groupBy(string $field, array $appendParams = [], ...$subGroups): self 896 | { 897 | $alias = $field . '_terms'; 898 | $params = array_merge(['field' => $field], $appendParams); 899 | return $this->aggs($alias, 'terms', $params, ... $subGroups); 900 | } 901 | 902 | /** 903 | * date_histogram 聚合 904 | * @param string $field 905 | * @param string $interval [year,quarter,month,week,day,hour,minute,second,1.5h,1M...] 906 | * @param string $format 年月日时分秒的表示方式 [yyyy-MM-dd HH:mm:ss] 907 | * @param array $appendParams 908 | * @param mixed ...$subGroups 909 | * @return $this 910 | */ 911 | public function dateGroupBy(string $field, string $interval = 'day', string $format = "yyyy-MM-dd", array $appendParams = [], ...$subGroups): self 912 | { 913 | $alias = $field . '_date_histogram'; 914 | $params = array_merge([ 915 | 'field' => $field, 916 | 'interval' => $interval, 917 | 'format' => $format, 918 | 'min_doc_count' => 0, 919 | ], $appendParams); 920 | return $this->aggs($alias, 'date_histogram', $params, ... $subGroups); 921 | } 922 | 923 | /** 924 | * @param string $field 925 | * @param array $appendParams 926 | * @return $this 927 | */ 928 | public function cardinality(string $field, array $appendParams = []): self 929 | { 930 | $alias = $field . '_cardinality'; 931 | $params = array_merge(['field' => $field], $appendParams); 932 | return $this->aggs($alias, 'cardinality', $params); 933 | } 934 | 935 | /** 936 | * @param string $field 937 | * @param array $appendParams 938 | * @return $this 939 | */ 940 | public function avg(string $field, array $appendParams = []): self 941 | { 942 | $alias = $field . '_avg'; 943 | $params = array_merge(['field' => $field], $appendParams); 944 | return $this->aggs($alias, 'avg', $params); 945 | } 946 | 947 | /** 948 | * @param string $field 949 | * @param array $appendParams 950 | * @return $this 951 | */ 952 | public function sum(string $field, array $appendParams = []): self 953 | { 954 | $alias = $field . '_sum'; 955 | $params = array_merge(['field' => $field], $appendParams); 956 | return $this->aggs($alias, 'sum', $params); 957 | } 958 | 959 | /** 960 | * @param string $field 961 | * @param array $appendParams 962 | * @return $this 963 | */ 964 | public function min(string $field, array $appendParams = []): self 965 | { 966 | $alias = $field . '_min'; 967 | $params = array_merge(['field' => $field], $appendParams); 968 | return $this->aggs($alias, 'min', $params); 969 | } 970 | 971 | /** 972 | * @param string $field 973 | * @param array $appendParams 974 | * @return $this 975 | */ 976 | public function max(string $field, array $appendParams = []): self 977 | { 978 | $alias = $field . '_max'; 979 | $params = array_merge(['field' => $field], $appendParams); 980 | return $this->aggs($alias, 'max', $params); 981 | } 982 | 983 | /** 984 | * @param string $field 985 | * @param array $appendParams 986 | * @return $this 987 | */ 988 | public function stats(string $field, array $appendParams = []): self 989 | { 990 | $alias = $field . '_stats'; 991 | $params = array_merge(['field' => $field], $appendParams); 992 | return $this->aggs($alias, 'stats', $params); 993 | } 994 | 995 | /** 996 | * @param string $field 997 | * @param array $appendParams 998 | * @return $this 999 | */ 1000 | public function extendedStats(string $field, array $appendParams = []): self 1001 | { 1002 | $alias = $field . '_extended_stats'; 1003 | $params = array_merge(['field' => $field], $appendParams); 1004 | return $this->aggs($alias, 'extended_stats', $params); 1005 | } 1006 | 1007 | /** 1008 | * @param array $appendParams 1009 | * [ 1010 | * 'size' => 1, 1011 | * 'sort' => ['news_posttime' => ['order' => 'desc']], 1012 | * '_source' => ['news_title','news_posttime','news_url'], 1013 | * 'highlight' => ['fields' => ['news_title' => new \stdClass(),'news_digest' => ['number_of_fragments' => 0]]] 1014 | * ] 1015 | * @return $this 1016 | */ 1017 | 1018 | /** 1019 | * @param $params 1020 | * @return $this 1021 | */ 1022 | public function topHits($params): self 1023 | { 1024 | if (!($params instanceof Closure) && !is_array($params)) { 1025 | throw new \InvalidArgumentException('非法参数'); 1026 | } 1027 | if ($params instanceof Closure) { 1028 | call_user_func($params, $query = $this->newQuery()); 1029 | $params = $query->dsl(); 1030 | } 1031 | return $this->aggs('top_hits', 'top_hits', $params); 1032 | } 1033 | 1034 | /** 1035 | * 聚合内部进行条件过滤 1036 | * @param string $alias 别名 1037 | * @param callable|array $wheres 1038 | * @param mixed ...$subGroups 1039 | * @return $this 1040 | */ 1041 | public function aggsFilter(string $alias, $wheres, ...$subGroups): self 1042 | { 1043 | return $this->aggs($alias, 'filter', $this->newQuery()->where($wheres), ... $subGroups); 1044 | } 1045 | 1046 | protected function addArrayOfWheres($field, $boolean = 'and', $not = false, $filter = false) 1047 | { 1048 | return $this->nestedQuery(function (self $query) use ($field, $not, $filter) { 1049 | foreach ($field as $key => $value) { 1050 | if (is_numeric($key) && is_array($value)) { 1051 | $query->where(...$value); 1052 | } else { 1053 | $query->where($key, '=', $value); 1054 | } 1055 | } 1056 | }, $boolean, $not, $filter); 1057 | } 1058 | 1059 | protected function nestedQuery(Closure $callback, $boolean = 'and', $not = false, $filter = false): self 1060 | { 1061 | call_user_func($callback, $query = $this->newQuery()); 1062 | if (!empty($query->wheres)) { 1063 | $type = 'nestedQuery'; 1064 | $this->wheres[] = compact('type', 'query', 'boolean', 'not', 'filter'); 1065 | } 1066 | return $this; 1067 | } 1068 | 1069 | protected function newQuery() 1070 | { 1071 | return new static(); 1072 | } 1073 | 1074 | protected function prepareValueAndOperator($value, $operator, $useDefault = false) 1075 | { 1076 | if ($useDefault) { 1077 | return [$operator, '=']; 1078 | } elseif (is_null($value) && in_array($operator, $this->operators)) { 1079 | throw new Exception('非法运算符和值组合'); 1080 | } elseif (is_array($value) && !in_array($operator, ['=', '!=', '<>'])) { 1081 | throw new Exception('非法运算符和值组合'); 1082 | } 1083 | return [$value, $operator]; 1084 | } 1085 | 1086 | /** 1087 | * @param $dsl 1088 | * @return $this 1089 | */ 1090 | public function raw($dsl) 1091 | { 1092 | $this->raw = $dsl; 1093 | return $this; 1094 | } 1095 | 1096 | 1097 | /** 1098 | * 返回dsl语句 1099 | * @param string $type 1100 | * @return array|false|string|null 1101 | */ 1102 | public function dsl($type = 'array') 1103 | { 1104 | if (!empty($this->raw)) { 1105 | $this->dsl = $this->raw; 1106 | } else { 1107 | $this->dsl = $this->grammar->compileComponents($this); 1108 | } 1109 | if (!is_string($this->dsl) && $type == 'json') { 1110 | $this->dsl = json_encode($this->dsl, JSON_UNESCAPED_UNICODE); 1111 | } 1112 | return $this->dsl; 1113 | } 1114 | 1115 | /** 1116 | * @return mixed 1117 | */ 1118 | public function response() 1119 | { 1120 | return $this->response; 1121 | } 1122 | 1123 | /** 1124 | * 返回文档总数、列表、聚合 1125 | * @param bool $directReturn 1126 | * @return array|null 1127 | */ 1128 | public function get($directReturn = false) 1129 | { 1130 | $this->runQuery(); 1131 | if ($directReturn) { 1132 | return $this->response; 1133 | } 1134 | try { 1135 | $list = array_map(function ($hit) { 1136 | return $this->decorate(array_merge([ 1137 | '_index' => $hit['_index'], 1138 | '_type' => $hit['_type'], 1139 | '_id' => $hit['_id'], 1140 | '_score' => $hit['_score'] 1141 | ], $hit['_source'], isset($hit['highlight']) ? ['highlight' => $hit['highlight']] : [])); 1142 | }, $this->response['hits']['hits']); 1143 | $data = [ 1144 | 'total' => $this->response['hits']['total'], 1145 | 'list' => $list 1146 | ]; 1147 | if (isset($this->response['aggregations'])) { 1148 | $data['aggs'] = $this->response['aggregations']; 1149 | //$data['aggs'] = $this->handleAgg($this,$this->response['aggregations']); 1150 | } 1151 | if (isset($this->response['_scroll_id'])) { 1152 | $data['scroll_id'] = $this->response['_scroll_id']; 1153 | } 1154 | return $data; 1155 | } catch (\Exception $e) { 1156 | throw new Exception('数据解析错误-' . $e->getMessage()); 1157 | } 1158 | } 1159 | 1160 | /** 1161 | * 分页查询 1162 | * @param int $page 1163 | * @param int $size 1164 | * @return array 1165 | */ 1166 | public function paginator(int $page = 1, int $size = 10) 1167 | { 1168 | $from = ($page - 1) * $size; 1169 | 1170 | $this->from($from); 1171 | 1172 | $this->size($size); 1173 | 1174 | if (!empty($this->collapse)) { 1175 | $collapseField = is_string($this->collapse) ? $this->collapse : $this->collapse['field']; 1176 | $this->cardinality($collapseField); 1177 | } 1178 | $this->runQuery(); 1179 | try { 1180 | $original_total = $total = $this->response['hits']['total']; 1181 | if (!empty($this->collapse)) { 1182 | $total = $this->response['aggregations'][$collapseField . '_cardinality']['value']; 1183 | } 1184 | $list = array_map(function ($hit) { 1185 | return $this->decorate(array_merge([ 1186 | '_index' => $hit['_index'], 1187 | '_type' => $hit['_type'], 1188 | '_id' => $hit['_id'], 1189 | '_score' => $hit['_score'] 1190 | ], $hit['_source'], isset($hit['highlight']) ? ['highlight' => $hit['highlight']] : [])); 1191 | }, $this->response['hits']['hits']); 1192 | $maxPage = intval(ceil($total / $size)); 1193 | $data = [ 1194 | 'total' => $total, 1195 | 'original_total' => $original_total, 1196 | 'per_page' => $size, 1197 | 'current_page' => $page, 1198 | 'last_page' => $maxPage, 1199 | 'list' => $list 1200 | ]; 1201 | if (isset($this->response['aggregations'])) { 1202 | $data['aggs'] = $this->response['aggregations']; 1203 | } 1204 | return $data; 1205 | } catch (\Exception $e) { 1206 | throw new Exception('数据解析错误-' . $e->getMessage()); 1207 | } 1208 | } 1209 | 1210 | /** 1211 | * @param bool $directReturn 1212 | * @return array|null 1213 | */ 1214 | public function first($directReturn = false) 1215 | { 1216 | $this->size(1); 1217 | $this->runQuery(); 1218 | if ($directReturn) { 1219 | return $this->response; 1220 | } 1221 | $data = null; 1222 | if (isset($this->response['hits']['hits'][0])) { 1223 | $hit = $this->response['hits']['hits'][0]; 1224 | $data = $this->decorate(array_merge([ 1225 | '_index' => $hit['_index'], 1226 | '_type' => $hit['_type'], 1227 | '_id' => $hit['_id'], 1228 | '_score' => $hit['_score'] 1229 | ], $hit['_source'], isset($hit['highlight']) ? ['highlight' => $hit['highlight']] : [])); 1230 | } 1231 | return $data; 1232 | } 1233 | 1234 | /** 1235 | * 返回命中的文档个数 1236 | * @return int 1237 | */ 1238 | public function count() 1239 | { 1240 | $this->runQuery(); 1241 | return $this->response['hits']['total']; 1242 | } 1243 | 1244 | /** 1245 | * 聚合结果处理(搁置) 1246 | * 暂时仅支持对【terms,histogram,date_histogram,filter,cardinality,avg,sum,min,max,extended_stats,top_hits】 1247 | * 如需拓展请在子类中重写此方法 1248 | * @param Builder $builder 1249 | * @param $aggsResponse 1250 | * @return array 1251 | */ 1252 | protected function handleAgg(Builder $builder, $aggsResponse) 1253 | { 1254 | $result = []; 1255 | if (empty($builder->aggs)) { 1256 | return $aggsResponse; 1257 | } 1258 | foreach ($builder->aggs as $agg) { 1259 | $item = []; 1260 | $key = $agg['alias']; 1261 | if (isset($aggsResponse[$key])) { 1262 | switch ($agg['type']) { 1263 | case 'terms': 1264 | case 'histogram': 1265 | case 'date_histogram': 1266 | case 'date_range': 1267 | case 'range': 1268 | $buckets = $aggsResponse[$key]['buckets']; 1269 | if (!empty($agg['subGroups'])) { 1270 | foreach ($agg['subGroups'] as $subGroup) { 1271 | foreach ($buckets as $k => $bucket) { 1272 | $buckets[$k] = array_merge($bucket, $this->handleAgg($subGroup, $bucket)); 1273 | } 1274 | } 1275 | } 1276 | $item = $buckets; 1277 | break; 1278 | case 'filter': 1279 | $item['doc_count'] = $aggsResponse[$key]['doc_count']; 1280 | if (!empty($agg['subGroups'])) { 1281 | foreach ($agg['subGroups'] as $subGroup) { 1282 | $item = array_merge($item, $this->handleAgg($subGroup, $aggsResponse[$key])); 1283 | } 1284 | } 1285 | break; 1286 | case 'cardinality': 1287 | case 'avg': 1288 | case 'sum': 1289 | case 'min': 1290 | case 'max': 1291 | $item = $aggsResponse[$key]['value']; 1292 | break; 1293 | case 'stats': 1294 | case 'extended_stats': 1295 | $item = $aggsResponse[$key]; 1296 | break; 1297 | case 'top_hits': 1298 | $item = array_map(function ($hit) { 1299 | return array_merge([ 1300 | '_index' => $hit['_index'], 1301 | '_type' => $hit['_type'], 1302 | '_id' => $hit['_id'], 1303 | '_score' => $hit['_score'] 1304 | ], $hit['_source'], isset($hit['highlight']) ? ['highlight' => $hit['highlight']] : []); 1305 | }, $aggsResponse[$key]['hits']['hits']); 1306 | break; 1307 | default: 1308 | // 太多了,头疼 1309 | $item = $aggsResponse[$key]; 1310 | break; 1311 | } 1312 | } 1313 | $result[$key] = $item; 1314 | } 1315 | return $result; 1316 | } 1317 | 1318 | /** 1319 | * 执行查询,并返回结果数组 1320 | * @return mixed 1321 | */ 1322 | protected function runQuery() 1323 | { 1324 | $this->dsl('json'); 1325 | if (empty($this->scroll)) { 1326 | $this->response = $this->query(); 1327 | } else { 1328 | $this->response = $this->scrollQuery(); 1329 | } 1330 | if (is_string($this->response)) { 1331 | $this->response = json_decode($this->response, true); 1332 | } 1333 | } 1334 | 1335 | /** 1336 | * @param $item 1337 | * @return mixed 1338 | */ 1339 | protected function decorate($item) 1340 | { 1341 | return $item; 1342 | } 1343 | 1344 | abstract public function query(); 1345 | 1346 | abstract public function scrollQuery(); 1347 | } 1348 | -------------------------------------------------------------------------------- /src/Grammar.php: -------------------------------------------------------------------------------- 1 | 'fields', 27 | 'collapse' => 'collapse', 28 | 'from' => 'from', 29 | 'size' => 'size', 30 | 'sort' => 'orders', 31 | 'query' => 'wheres', 32 | 'post_filter' => 'postWheres', 33 | 'aggs' => 'aggs', 34 | 'highlight' => 'highlight', 35 | ]; 36 | 37 | protected $operatorMappings = [ 38 | '>' => 'gt', 39 | '>=' => 'gte', 40 | '<' => 'lt', 41 | '<=' => 'lte', 42 | ]; 43 | 44 | public function compileComponents($builder) 45 | { 46 | $dsl = []; 47 | foreach ($this->selectComponents as $k => $v) { 48 | if (!is_null($builder->$v)) { 49 | $method = 'compile' . ucfirst($v); 50 | $dsl[$k] = $this->$method($builder); 51 | } 52 | } 53 | return empty($dsl) ? new \stdClass() : $dsl; 54 | } 55 | 56 | public function compileFields($builder): array 57 | { 58 | return $builder->fields; 59 | } 60 | 61 | public function compileWheres($builder, $filter = false, $not = false): array 62 | { 63 | $whereGroups = $this->wherePriorityGroup($builder->wheres); 64 | $operation = count($whereGroups) === 1 ? 'must' : 'should'; 65 | $bool = []; 66 | foreach ($whereGroups as $wheres) { 67 | $boolMust = $boolMustNot = $boolFilter = []; 68 | foreach ($wheres as $where) { 69 | if ($where['type'] === 'nestedQuery') { 70 | $tmp = $this->compileWheres($where['query'], $where['filter'], $where['not']); 71 | } else { 72 | $tmp = $this->whereMatch($where); 73 | } 74 | if ($where['filter']) { 75 | $boolFilter[] = $tmp; 76 | } elseif ($where['not']) { 77 | $boolMustNot[] = $tmp; 78 | } else { 79 | $boolMust[] = $tmp; 80 | } 81 | } 82 | if ($operation == 'should') { 83 | $bool['bool'][$operation] = $bool['bool'][$operation] ?? []; 84 | $tmp = []; 85 | if (!empty($boolMust)) { 86 | if (count($boolMust) === 1 && empty($boolMustNot) && empty($boolFilter)) { 87 | $tmp = $boolMust[0]; 88 | } else { 89 | $tmp['bool']['must'] = $boolMust; 90 | } 91 | } 92 | if (!empty($boolMustNot)) { 93 | $tmp['bool']['must_not'] = $boolMustNot; 94 | } 95 | if (!empty($boolFilter)) { 96 | $tmp['bool']['filter'] = $boolFilter; 97 | } 98 | array_push($bool['bool'][$operation], $tmp); 99 | } else { 100 | if (!empty($boolMust)) { 101 | if (count($boolMust) === 1 && empty($boolMustNot) && empty($boolFilter)) { 102 | $bool = $boolMust[0]; 103 | } else { 104 | $bool['bool']['must'] = $boolMust; 105 | } 106 | } 107 | if (!empty($boolMustNot)) { 108 | $bool['bool']['must_not'] = $boolMustNot; 109 | } 110 | if (!empty($boolFilter)) { 111 | $bool['bool']['filter'] = $boolFilter; 112 | } 113 | } 114 | } 115 | if (!is_null($builder->minimumShouldMatch)) { 116 | $bool['bool']['minimum_should_match'] = $builder->minimumShouldMatch; 117 | } 118 | if ($filter && $not) { 119 | $bool = [ 120 | 'bool' => [ 121 | 'must_not' => $bool 122 | ] 123 | ]; 124 | } 125 | return $bool; 126 | } 127 | 128 | public function compilePostWheres($builder, $filter = false, $not = false): array 129 | { 130 | $whereGroups = $this->wherePriorityGroup($builder->postWheres); 131 | $operation = count($whereGroups) === 1 ? 'must' : 'should'; 132 | $bool = []; 133 | foreach ($whereGroups as $wheres) { 134 | $boolMust = $boolMustNot = $boolFilter = []; 135 | foreach ($wheres as $where) { 136 | if ($where['type'] === 'nestedQuery') { 137 | $tmp = $this->compileWheres($where['query'], $where['filter'], $where['not']); 138 | } else { 139 | $tmp = $this->whereMatch($where); 140 | } 141 | if ($where['filter']) { 142 | $boolFilter[] = $tmp; 143 | } elseif ($where['not']) { 144 | $boolMustNot[] = $tmp; 145 | } else { 146 | $boolMust[] = $tmp; 147 | } 148 | } 149 | if ($operation == 'should') { 150 | $bool['bool'][$operation] = $bool['bool'][$operation] ?? []; 151 | $tmp = []; 152 | if (!empty($boolMust)) { 153 | if (count($boolMust) === 1 && empty($boolMustNot) && empty($boolFilter)) { 154 | $tmp = $boolMust[0]; 155 | } else { 156 | $tmp['bool']['must'] = $boolMust; 157 | } 158 | } 159 | if (!empty($boolMustNot)) { 160 | $tmp['bool']['must_not'] = $boolMustNot; 161 | } 162 | if (!empty($boolFilter)) { 163 | $tmp['bool']['filter'] = $boolFilter; 164 | } 165 | array_push($bool['bool'][$operation], $tmp); 166 | } else { 167 | if (!empty($boolMust)) { 168 | if (count($boolMust) === 1 && empty($boolMustNot) && empty($boolFilter)) { 169 | $bool = $boolMust[0]; 170 | } else { 171 | $bool['bool']['must'] = $boolMust; 172 | } 173 | } 174 | if (!empty($boolMustNot)) { 175 | $bool['bool']['must_not'] = $boolMustNot; 176 | } 177 | if (!empty($boolFilter)) { 178 | $bool['bool']['filter'] = $boolFilter; 179 | } 180 | } 181 | } 182 | if (!is_null($builder->minimumShouldMatch)) { 183 | $bool['bool']['minimum_should_match'] = $builder->minimumShouldMatch; 184 | } 185 | if ($filter && $not) { 186 | $bool = [ 187 | 'bool' => [ 188 | 'must_not' => $bool 189 | ] 190 | ]; 191 | } 192 | return $bool; 193 | } 194 | 195 | public function compileAggs(Builder $builder): array 196 | { 197 | $aggs = []; 198 | foreach ($builder->aggs as $agg) { 199 | $params = $agg['params']; 200 | if ($agg['params'] instanceof Builder) { 201 | $params = $this->compileWheres($agg['params']); 202 | } 203 | $aggs[$agg['alias']] = [$agg['type'] => $params]; 204 | if (!empty($agg['subGroups'])) { 205 | $aggs[$agg['alias']]['aggs'] = []; 206 | foreach ($agg['subGroups'] as $subGroup) { 207 | $aggs[$agg['alias']]['aggs'] = array_merge($aggs[$agg['alias']]['aggs'], $this->compileAggs($subGroup)); 208 | } 209 | } 210 | } 211 | return $aggs; 212 | } 213 | 214 | public function compileOrders($builder): array 215 | { 216 | return $builder->orders; 217 | } 218 | 219 | public function compileSize($builder) 220 | { 221 | return $builder->size; 222 | } 223 | 224 | public function compileFrom($builder) 225 | { 226 | return $builder->from; 227 | } 228 | 229 | public function compileIndex($builder) 230 | { 231 | return $builder->index; 232 | } 233 | 234 | public function compileType($builder) 235 | { 236 | return $builder->type; 237 | } 238 | 239 | public function compileCollapse($builder) 240 | { 241 | if (is_string($builder->collapse)) { 242 | $collapse = ['field' => $builder->collapse]; 243 | } else { 244 | $collapse = $builder->collapse; 245 | } 246 | return $collapse; 247 | } 248 | 249 | public function compileHighlight($builder) 250 | { 251 | $highlight = $builder->highlightConfig; 252 | foreach ($builder->highlight as $field => $params) { 253 | $highlight['fields'][$field] = empty($params) ? new \stdClass() : $params; 254 | } 255 | return $highlight; 256 | } 257 | 258 | public function whereMatch(array $where) 259 | { 260 | $term = []; 261 | switch ($where['type']) { 262 | case 'basic': 263 | $term = ['term' => [$where['field'] => $where['value']]]; 264 | break; 265 | case 'match': 266 | case 'match_phrase': 267 | case 'match_phrase_prefix': 268 | $term = [ 269 | $where['type'] => [ 270 | $where['field'] => [ 271 | 'query' => $where['value'] 272 | ] 273 | ] 274 | ]; 275 | if (!empty($where['appendParams'])) { 276 | $term[$where['type']][$where['field']] = array_merge($term[$where['type']][$where['field']], $where['appendParams']); 277 | } 278 | break; 279 | case 'multi_match': 280 | $term = [ 281 | 'multi_match' => [ 282 | 'query' => $where['value'], 283 | 'type' => $where['matchType'], 284 | 'fields' => $where['field'] 285 | ] 286 | ]; 287 | if (!empty($where['appendParams'])) { 288 | $term['multi_match'] = array_merge($term['multi_match'], $where['appendParams']); 289 | } 290 | break; 291 | case 'between': 292 | $term = []; 293 | foreach ($where['value'] as $k => $v) { 294 | if (is_numeric($k)) { 295 | if ($k == 0) { 296 | $term['range'][$where['field']][$this->operatorMappings['>=']] = $v; 297 | } else { 298 | $term['range'][$where['field']][$this->operatorMappings['<=']] = $v; 299 | } 300 | } else { 301 | $term['range'][$where['field']][$this->operatorMappings[$k]] = $v; 302 | } 303 | } 304 | break; 305 | case 'in': 306 | $term = ['terms' => [$where['field'] => $where['value']]]; 307 | break; 308 | case 'exists': 309 | $term = ['exists' => ['field' => $where['field']]]; 310 | break; 311 | case 'prefix': 312 | case 'wildcard': 313 | case 'regexp': 314 | case 'fuzzy': 315 | $term = [ 316 | $where['type'] => [ 317 | $where['field'] => [ 318 | 'value' => $where['value'] 319 | ] 320 | ] 321 | ]; 322 | if (!empty($where['appendParams'])) { 323 | $term[$where['type']][$where['field']] = array_merge($term[$where['type']][$where['field']], $where['appendParams']); 324 | } 325 | break; 326 | case 'nested': 327 | $term = [ 328 | 'nested' => [ 329 | 'path' => $where['path'], 330 | 'query' => $this->compileWheres($where['query'], $where['filter'], $where['not']) 331 | ] 332 | ]; 333 | if (!empty($where['appendParams'])) { 334 | $term['nested'] = array_merge($term['nested'], $where['appendParams']); 335 | } 336 | break; 337 | case 'raw': 338 | $term = $where['where']; 339 | break; 340 | } 341 | if (@$where['filter'] && $where['not']) { 342 | $term = ['bool' => ['must_not' => $term]]; 343 | } 344 | return $term; 345 | } 346 | 347 | public function wherePriorityGroup($wheres): array 348 | { 349 | $orIndex = array_keys(array_map(function ($where) { 350 | return $where['boolean']; 351 | }, $wheres), 'or'); 352 | $initIndex = $lastIndex = 0; 353 | $groups = []; 354 | foreach ($orIndex as $index) { 355 | $items = array_slice($wheres, $initIndex, $index - $initIndex); 356 | if ($items) $groups[] = $items; 357 | $initIndex = $lastIndex = $index; 358 | } 359 | $groups[] = array_slice($wheres, $lastIndex); 360 | return $groups; 361 | } 362 | } 363 | --------------------------------------------------------------------------------