Completed
Push — devel ( ccf8ba...513da0 )
by Alexey
01:58
created

Query::setOrderBy()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 4
nc 1
nop 2
1
<?php
2
3
namespace Bardex\Elastic;
4
5
6
class Query implements \JsonSerializable
7
{
8
    /**
9
     * @var \Elasticsearch\Client $client
10
     */
11
    protected $elastic;
12
13
    /**
14
     * Параметры запроса
15
     * @var array
16
     */
17
    protected $params=[];
18
19
    /**
20
     * сколько всего в индексе ES строк удовлетворяющих параметрам поиска
21
     * @var integer $totalResults
22
     */
23
    protected $totalResults;
24
25
    /**
26
     * Логгер
27
     * @var \Psr\Log\LoggerInterface $logger
28
     */
29
    protected $logger;
30
31
32
    public function __construct(\Elasticsearch\Client $elastic)
33
    {
34
        $this->elastic = $elastic;
35
        $this->logger = new \Psr\Log\NullLogger;
36
    }
37
38
39
    public function setLogger(\Psr\Log\LoggerInterface $logger)
40
    {
41
        $this->logger = $logger;
42
        return $this;
43
    }
44
45
46
    /**
47
     * Установить имя индекса для поиска
48
     * @param $index
49
     * @return $this
50
     */
51
    public function setIndex($index)
52
    {
53
        $this->params['index'] = (string) $index;
54
        return $this;
55
    }
56
57
58
    /**
59
     * Установить имя типа для поиска
60
     * @param $type
61
     * @return $this
62
     */
63
    public function setType($type)
64
    {
65
        $this->params['type'] = (string) $type;
66
        return $this;
67
    }
68
69
70
    /**
71
     * Выводить перечисленные поля.
72
     * (не обязательный метод, по-умолчанию, выводятся все)
73
     * Методы select() и exclude() могут работать совместно.
74
     * @param array $fields
75
     * @return $this;
0 ignored issues
show
Documentation introduced by
The doc-type $this; could not be parsed: Expected "|" or "end of type", but got ";" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
76
     * @example $q->select(['id', 'title', 'brand.id', 'brand.title']);
77
     */
78
    public function select(array $fields)
79
    {
80
        $this->params['body']['_source']['includes'] = $fields;
81
        return $this;
82
    }
83
84
85
    /**
86
     * Добавить в результаты вычисляемое поле, на скриптовом языке painless или groovy
87
     * ```
88
     * $q->addScriptField('pricefactor', 'return doc["product.price"].value * params.factor', ['factor' => 2]);
89
     * ```
90
     * Использование параметров рекомендуется, для увеличения производительности и эффективности компилирования скриптов.
91
     * @param string $fieldName - имя поля в результатах (если такое поле уже есть в документе, то оно будет заменено)
92
     * @param string $script - текст скрипта
93
     * @param array $params - параметры которые нужно передать в скрипт
94
     * @param string $lang - язык скрипта painless или groovy
95
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/5.0/search-request-script-fields.html
96
     * @return self $this
97
     */
98
    public function addScriptField($fieldName, $script, array $params = null, $lang = 'painless')
99
    {
100
        $item = [
101
            'script' => [
102
                'lang'   => $lang,
103
                'inline' => $script,
104
            ]
105
        ];
106
        if ($params) {
107
            $item['script']['params'] = $params;
108
        }
109
        $this->params['body']['script_fields'][$fieldName] = $item;
110
        return $this;
111
    }
112
113
114
    /**
115
     * Удалить из выборки поля.
116
     * (не обязательный метод, по-умолчанию, выводятся все)
117
     * Методы select() и exclude() могут работать совместно.
118
     * @param array $fields
119
     * @return $this;
0 ignored issues
show
Documentation introduced by
The doc-type $this; could not be parsed: Expected "|" or "end of type", but got ";" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
120
     * @example $q->exclude(['anons', '*.anons']);
121
     */
122
    public function exclude(array $fields)
123
    {
124
        $this->params['body']['_source']['excludes'] = $fields;
125
        return $this;
126
    }
127
128
129
    /**
130
     * Добавить фильтр в raw формате, если готовые методы фильтрации не подходят.
131
     * Для удобства используй готовые методы фильтрации: where(), whereIn(), whereBetween(), whereMatch()
132
     * whereLess() и другие методы where*()
133
     *
134
     * @param $type - тип фильтрации (term|terms|match|range)
135
     * @param $filter - сам фильтр
136
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/5.0/query-dsl-terms-query.html
137
     * @return $this
138
     */
139
    public function addFilter($type, $filter)
140
    {
141
        if ( !isset($this->params['body']['query']['bool']['must']) ) {
142
            $this->params['body']['query']['bool']['must'] = [];
143
        }
144
145
        $this->params['body']['query']['bool']['must'][] = [$type => $filter];
146
147
        return $this;
148
    }
149
150
151
    /**
152
     * Добавить фильтр ТОЧНОГО совпадения,
153
     * этот фильтр не влияет на поле релевантности _score.
154
     *
155
     * @param $field - поле по которому фильтруем (id, page.categoryId...)
156
     * @param $value - искомое значение
157
     * @example $q->where('channel', 1)->where('page.categoryId', 10);
158
     * @return $this;
0 ignored issues
show
Documentation introduced by
The doc-type $this; could not be parsed: Expected "|" or "end of type", but got ";" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
159
     */
160
    public function where($field, $value)
161
    {
162
        $this->addFilter('term', [$field => $value]);
163
        return $this;
164
    }
165
166
167
    /**
168
     * Добавить фильтр совпадения хотя бы одного значения из набора,
169
     * этот фильтр не влияет на поле релевантности _score.
170
     *
171
     * @param $field - поле по которому фильтруем
172
     * @param $values - массив допустимых значений
173
     * @example $q->whereIn('channel', [1,2,3])->whereIn('page.categoryId', [10,11]);
174
     * @return $this;
0 ignored issues
show
Documentation introduced by
The doc-type $this; could not be parsed: Expected "|" or "end of type", but got ";" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
175
     */
176
    public function whereIn($field, array $values)
177
    {
178
        // потому что ES не понимает дырки в ключах
179
        $values = array_values($values);
180
        $this->addFilter('terms', [$field => $values]);
181
        return $this;
182
    }
183
184
185
    /**
186
     * Добавить фильтр вхождения значение в диапазон (обе границы включительно)
187
     * Можно искать по диапазону дат
188
     * этот фильтр не влияет на поле релевантности _score.
189
     *
190
     * @param $field - поле, по которому фильтруем
191
     * @param $min - нижняя граница диапазона (включительно)
192
     * @param $max - верхняя граница диапазона (включительно)
193
     * @param $dateFormat - необязательное поле описание формата даты
194
     * @example $q->whereBetween('created', '01/01/2010','01/01/2011', 'dd/MM/yyyy');
195
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/5.0/query-dsl-range-query.html
196
     * @return $this;
0 ignored issues
show
Documentation introduced by
The doc-type $this; could not be parsed: Expected "|" or "end of type", but got ";" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
197
     */
198 View Code Duplication
    public function whereBetween($field, $min, $max, $dateFormat = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
199
    {
200
        $params = ['gte' => $min, 'lte' => $max];
201
        if ($dateFormat) {
202
            $params['format'] = $dateFormat;
203
        }
204
        $this->addFilter('range',[$field => $params]);
205
        return $this;
206
    }
207
208
209
    /**
210
     * Добавить в фильтр сложное условие с вычислениями, на скриптовом языке painless или groovy
211
     * ```
212
     *  $q->whereScript('doc["id"].value == params.id', ['id' => 5169]);
213
     * ```
214
     * Использование параметров рекомендуется, для увеличения производительности и эффективности компилирования скриптов
215
     *
216
     * @param string $script - строка скрипта
217
     * @param array $params - параматеры для скрипта
218
     * @param string $lang - язык painless или groovy
219
     * @return self $this;
220
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/5.0/query-dsl-script-query.html
221
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/5.0/modules-scripting-painless.html
222
     */
223
    public function whereScript($script, array $params = null, $lang = 'painless')
224
    {
225
        $item = [
226
            'script' => [
227
                'inline' => $script,
228
                'lang'   => $lang
229
            ]
230
        ];
231
        if ($params) {
232
            $item['script']['params'] = $params;
233
        }
234
        $this->addFilter('script', $item);
235
        return $this;
236
    }
237
238
239
    /**
240
     * добавить фильтр "больше или равно"
241
     * @param $field
242
     * @param $value
243
     * @param null $dateFormat
244
     * @return $this
245
     */
246 View Code Duplication
    public function whereGreaterOrEqual($field, $value, $dateFormat = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
247
    {
248
        $params = ['gte' => $value];
249
        if ($dateFormat) {
250
            $params['format'] = $dateFormat;
251
        }
252
        $this->addFilter('range', [$field => $params]);
253
        return $this;
254
    }
255
256
    /**
257
     * добавить фильтр "больше чем"
258
     * @param $field
259
     * @param $value
260
     * @param null $dateFormat
261
     * @return $this
262
     */
263 View Code Duplication
    public function whereGreater($field, $value, $dateFormat = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
264
    {
265
        $params = ['gt' => $value];
266
        if ($dateFormat) {
267
            $params['format'] = $dateFormat;
268
        }
269
        $this->addFilter('range', [$field => $params]);
270
        return $this;
271
    }
272
273
    /**
274
     * добавить фильтр "меньше или равно"
275
     * @param $field
276
     * @param $value
277
     * @param null $dateFormat
278
     * @return $this
279
     */
280 View Code Duplication
    public function whereLessOrEqual($field, $value, $dateFormat = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
281
    {
282
        $params = ['lte' => $value];
283
        if ($dateFormat) {
284
            $params['format'] = $dateFormat;
285
        }
286
        $this->addFilter('range', [$field => $params]);
287
        return $this;
288
    }
289
290
    /**
291
     * добавить фильтр "меньше чем"
292
     * @param $field
293
     * @param $value
294
     * @param null $dateFormat
295
     * @return $this
296
     */
297 View Code Duplication
    public function whereLess($field, $value, $dateFormat = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
298
    {
299
        $params = ['lt' => $value];
300
        if ($dateFormat) {
301
            $params['format'] = $dateFormat;
302
        }
303
        $this->addFilter('range', [$field => $params]);
304
        return $this;
305
    }
306
307
308
    /**
309
     * Добавить фильтр полнотекстового поиска
310
     * этот фильтр влияет на поле релевантности _score.
311
     *
312
     * @param $field - поле по которому фильтруем
313
     * @param $text - поисковая фраза
314
     * @example $q->whereMatch('title', 'яблочная слойка')->setOrderBy('_score', 'desc');
315
     * @return $this;
0 ignored issues
show
Documentation introduced by
The doc-type $this; could not be parsed: Expected "|" or "end of type", but got ";" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
316
     */
317
    public function whereMatch($field, $text)
318
    {
319
        if (is_array($field)) {
320
            $this->addFilter('multi_match', [
321
                    'query'  => $text,
322
                    'fields' => $field
323
                ]);
324
        }
325
        else {
326
            $this->addFilter('match', [$field => $text]);
327
        }
328
        return $this;
329
    }
330
331
332
    /**
333
     * Добавить поле сортировки.
334
     * Для сортировки по релевантности существует псевдополе _score (значение больше - релевантность лучше)
335
     * @param $field - поле сортировки
336
     * @param string $order - направление сортировки asc|desc
337
     * @example $q->addOrderBy('_score', 'desc');
338
     * @return $this
339
     */
340
    public function addOrderBy($field, $order = 'asc')
341
    {
342
        $field = (string) $field;
343
        $order = (string) $order;
344
        if (!isset($this->params['body']['sort'])) {
345
            $this->params['body']['sort'] = [];
346
        }
347
        $this->params['body']['sort'][] = [$field => ['order' => $order]];
348
        return $this;
349
    }
350
351
352
    /**
353
     * Установить поле сортировки.
354
     * Для сортировки по релевантности существует псевдополе _score (значение больше - релевантность лучше)
355
     * @param $field - поле сортировки
356
     * @param string $order - направление сортировки asc|desc
357
     * @example $q->setOrderBy('_score', 'desc');
358
     * @return $this
359
     */
360
    public function setOrderBy($field, $order = 'asc')
361
    {
362
        $this->params['body']['sort'] = [];
363
        $this->addOrderBy($field, $order);
364
        return $this;
365
    }
366
367
368
    /**
369
     * Установить лимиты выборки
370
     * @param $limit - сколько строк выбирать
371
     * @param int $offset - сколько строк пропустить
372
     * @return $this;
0 ignored issues
show
Documentation introduced by
The doc-type $this; could not be parsed: Expected "|" or "end of type", but got ";" at position 5. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
373
     */
374
    public function limit($limit, $offset = 0)
375
    {
376
        $this->params['size'] = (int) $limit;
377
        $this->params['from'] = (int) $offset;
378
        return $this;
379
    }
380
381
382
    public function fetchRaw()
383
    {
384
        $this->totalResults;
385
386
        // build query
387
        $query  = $this->getQuery();
388
389
        // send query to elastic
390
        $start  = microtime(1);
391
392
        $result = $this->elastic->search($query);
393
394
        // measure time
395
        $time   = round((microtime(1) - $start) * 1000);
396
397
        // total results
398
        $this->totalResults = $result['hits']['total'];
399
400
        // log
401
        $index = $this->params['index'].'/'.$this->params['type'];
402
        $context = [
403
            'type'  => 'elastic',
404
            'query' => json_encode($query),
405
            'time'  => $time,
406
            'index' => $index,
407
            'found_rows'   => $this->totalResults,
408
            'fetched_rows' => count($result['hits']['hits'])
409
        ];
410
411
        $this->logger->debug("Elastic query (index: $index, time: $time ms)", $context);
412
413
        return $result;
414
    }
415
416
417
    /**
418
     * Выполнить запрос к ES и вернуть результаты поиска.
419
     * Внимание! для экономии памяти результаты не хранятся в этом объекте, а сразу возвращаются.
420
     * Чтобы получить кол-во строк всего найденных в индексе (без учета лимита), используй метод getTotalResults()
421
     * @return array - возвращает набор документов
422
     */
423
    public function fetchAll()
424
    {
425
        $result = $this->fetchRaw();
426
427
        $results = [];
428
        foreach ($result['hits']['hits'] as $hit) {
429
            $row = $hit['_source'];
430
            if (isset($hit['fields'])) { // script fields
431
                foreach ($hit['fields'] as $field => $data) {
432
                    if (count($data) == 1) {
433
                        $row[$field] = array_shift($data);
434
                    }
435
                    else {
436
                        $row[$field] = $data;
437
                    }
438
                }
439
            }
440
            $results[] = $row;
441
        }
442
443
        return $results;
444
    }
445
446
447
    /**
448
     * Выполнить запрос к ES и вернуть первый результат.
449
     * Внимание! для экономии памяти результаты не хранятся в этом объекте, а сразу возвращаются.
450
     * Чтобы получить кол-во строк всего найденных в индексе (без учета лимита), используй метод getTotalResults()
451
     * @return array|null возращает первый найденный документ или null.
452
     */
453
    public function fetchOne()
454
    {
455
        $results = $this->fetchAll();
456
        if (count($results)) {
457
            return array_shift($results);
458
        }
459
        else {
460
            return null;
461
        }
462
    }
463
464
465
    /**
466
     * Количество документов всего найденных в индексе, для последнего запроса.
467
     * @return int
468
     */
469
    public function getTotalResults()
470
    {
471
        return $this->totalResults;
472
    }
473
474
475
    /**
476
     * Собрать запрос
477
     * @return array
478
     */
479
    public function getQuery()
480
    {
481
        $params = $this->params;
482
483
        if (!isset($params['body']['_source'])) {
484
            $params['body']['_source'] = true;
485
        }
486
487
        return $params;
488
    }
489
490
491
    public function jsonSerialize() {
492
        return $this->getQuery();
493
    }
494
495
496
    /**
497
     * Получить JSON-дамп запроса для отладки
498
     * @return string
499
     */
500
    public function getJsonQuery()
501
    {
502
        return json_encode($this);
503
    }
504
}