├── .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 |
--------------------------------------------------------------------------------