Completed
Push — devel ( 8d3e4d...9aaeb0 )
by Alexey
02:35
created

Query::setLogger()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 9.4285
c 0
b 0
f 0
cc 1
eloc 3
nc 1
nop 1
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
     * имя индекса модели в ES
15
     * @var string $index
16
     */
17
    protected $index;
18
19
    /**
20
     * имя типа модели в ES
21
     * @var string $type
22
     */
23
    protected $type;
24
25
    /**
26
     * параметры сортировки
27
     * @var array $orders
28
     */
29
    protected $orders = [];
30
31
    /**
32
     * фильтры выборки
33
     * @var array $filters
34
     */
35
    protected $filters = [];
36
37
    /**
38
     * сколько всего в индексе ES строк удовлетворяющих параметрам поиска
39
     * @var integer $totalResults
40
     */
41
    protected $totalResults;
42
43
    /**
44
     * сколько строк выбирать из индекса
45
     * @var int $limit
46
     */
47
    protected $limit = 10;
48
49
    /**
50
     * сколько строк пропустить
51
     * @var int $offset
52
     */
53
    protected $offset = 0;
54
55
    /**
56
     * Какие поля выводить
57
     * @var array
58
     */
59
    protected $includes;
60
61
    /**
62
     * Какие поля исключить из выборки
63
     * @var array
64
     */
65
    protected $excludes;
66
67
    /**
68
     * Вычисляемые поля в результатах
69
     * @var array
70
     */
71
    protected $scriptFields = [];
72
73
    /**
74
     * Логгер
75
     * @var \Psr\Log\LoggerInterface $logger
76
     */
77
    protected $logger;
78
79
    public function __construct(\Elasticsearch\Client $elastic)
80
    {
81
        $this->elastic = $elastic;
82
        $this->logger = new \Psr\Log\NullLogger;
83
    }
84
85
    public function setLogger(\Psr\Log\LoggerInterface $logger)
86
    {
87
        $this->logger = $logger;
88
        return $this;
89
    }
90
91
    /**
92
     * Установить имя индекса для поиска
93
     * @param $index
94
     * @return $this
95
     */
96
    public function setIndex($index)
97
    {
98
        $this->index = (string) $index;
99
        return $this;
100
    }
101
102
    /**
103
     * Установить имя типа для поиска
104
     * @param $type
105
     * @return $this
106
     */
107
    public function setType($type)
108
    {
109
        $this->type = (string) $type;
110
        return $this;
111
    }
112
113
    /**
114
     * Выводить перечисленные поля.
115
     * (не обязательный метод, по-умолчанию, выводятся все)
116
     * Методы select() и exclude() могут работать совместно.
117
     * @param array $fields
118
     * @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...
119
     * @example $q->select(['id', 'title', 'brand.id', 'brand.title']);
120
     */
121
    public function select(array $fields)
122
    {
123
        $this->includes = $fields;
124
        return $this;
125
    }
126
127
128
    /**
129
     * Добавить в результаты вычисляемое поле, на скриптовом языке painless или groovy
130
     * ```
131
     * $q->addScriptField('timeshift', 'return doc["tvpDouble.timeshift"].value * params.factor', ['factor' => 2]);
132
     * ```
133
     * Использование параметров рекомендуется, для увеличения производительности и эффективности компилирования скриптов.
134
     * @param string $fieldName - имя поля в результатах (если такое поле уже есть в документе, то оно будет заменено)
135
     * @param string $script - текст скрипта
136
     * @param array $params - параметры которые нужно передать в скрипт
137
     * @param string $lang - язык скрипта painless или groovy
138
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/5.0/search-request-script-fields.html
139
     * @return self $this
140
     */
141 View Code Duplication
    public function addScriptField($fieldName, $script, array $params = null, $lang = 'painless')
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...
142
    {
143
        $item = [
144
            'script' => [
145
                'lang'   => $lang,
146
                'inline' => $script,
147
            ]
148
        ];
149
        if ($params) {
150
            $item['script']['params'] = $params;
151
        }
152
        $this->scriptFields[$fieldName] = $item;
153
        return $this;
154
    }
155
156
    /**
157
     * Удалить из выборки поля.
158
     * (не обязательный метод, по-умолчанию, выводятся все)
159
     * Методы select() и exclude() могут работать совместно.
160
     * @param array $fields
161
     * @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...
162
     * @example $q->exclude(['body', '*.body']);
163
     */
164
    public function exclude(array $fields)
165
    {
166
        $this->excludes = $fields;
167
        return $this;
168
    }
169
170
    /**
171
     * Добавить фильтр в raw формате, если готовые методы фильтрации не подходят.
172
     * Для удобства используй готовые методы фильтрации: where(), whereIn(), whereBetween(), whereMatch()
173
     * whereLess() и другие методы where*()
174
     *
175
     * @param $type - тип фильтрации (term|terms|match|range)
176
     * @param $filter - сам фильтр
177
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/5.0/query-dsl-terms-query.html
178
     * @return $this
179
     */
180
    public function addFilter($type, $filter)
181
    {
182
        $this->filters[] = [$type => $filter];
183
        return $this;
184
    }
185
186
    /**
187
     * Добавить фильтр ТОЧНОГО совпадения,
188
     * этот фильтр не влияет на поле релевантности _score.
189
     * Внимание! Класс Query не делает фильтрации или экранирования вводимых значений.
190
     *
191
     * @param $field - поле по которому фильтруем (id, brand.hasManySeries ...)
192
     * @param $value - искомое значение
193
     * @example $q->where('channel', 1)->where('tvpDouble.isDefault', 1);
194
     * @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...
195
     */
196
    public function where($field, $value)
197
    {
198
        $this->filters[] = ['term' => [$field => $value]];
199
        return $this;
200
    }
201
202
    /**
203
     * Добавить фильтр совпадения хотя бы одного значения из набора,
204
     * этот фильтр не влияет на поле релевантности _score.
205
     * Внимание! Класс Query не делает фильтрации или экранирования вводимых значений.
206
     *
207
     * @param $field - поле по которому фильтруем (id, brand.hasManySeries ...)
208
     * @param $values - массив допустимых значений
209
     * @example $q->whereIn('channel', [1,2,3])->where('tvpDouble.isDefault', 1);
210
     * @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...
211
     */
212
    public function whereIn($field, array $values)
213
    {
214
        // потому что ES не понимает дырки в ключах
215
        $values = array_values($values);
216
        $this->filters[] = ['terms' => [$field => $values]];
217
        return $this;
218
    }
219
220
    /**
221
     * Добавить фильтр вхождения значение в диапазон (обе границы включительно)
222
     * Можно искать по диапазону дат
223
     * этот фильтр не влияет на поле релевантности _score.
224
     * Внимание! Класс Query не делает фильтрации или экранирования вводимых значений.
225
     *
226
     * @param $field - поле, по которому фильтруем (realDateStart, ...)
227
     * @param $min - нижняя граница диапазона (включительно)
228
     * @param $max - верхняя граница диапазона (включительно)
229
     * @param $dateFormat - необязательное поле описание формата даты
230
     * @example
231
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/5.0/query-dsl-range-query.html
232
     * @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...
233
     */
234 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...
235
    {
236
        $params = ['gte' => $min, 'lte' => $max];
237
        if ($dateFormat) {
238
            $params['format'] = $dateFormat;
239
        }
240
        $this->filters[] = ['range' => [$field => $params]];
241
        return $this;
242
    }
243
244
    /**
245
     * Добавить в фильтр сложное условие с вычислениями, на скриптовом языке painless или groovy
246
     * ```
247
     *  $q->whereScript('doc["brand.id"].value == params.id', ['id' => 5169]);
248
     * ```
249
     * Использование параметров рекомендуется, для увеличения производительности и эффективности компилирования скриптов
250
     *
251
     * @param string $script - строка скрипта
252
     * @param array $params - параматеры для скрипта
253
     * @param string $lang - язык painless или groovy
254
     * @return self $this;
255
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/5.0/query-dsl-script-query.html
256
     * @link https://www.elastic.co/guide/en/elasticsearch/reference/5.0/modules-scripting-painless.html
257
     */
258 View Code Duplication
    public function whereScript($script, array $params = null, $lang = 'painless')
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...
259
    {
260
        $item = [
261
            'script' => [
262
                'script' => [
263
                    'inline' => $script,
264
                    'lang'   => $lang
265
                ]
266
            ]
267
        ];
268
        if ($params) {
269
            $item['script']['script']['params'] = $params;
270
        }
271
        $this->filters[] = $item;
272
        return $this;
273
    }
274
275
    /**
276
     * добавить фильтр "больше или равно"
277
     * @param $field
278
     * @param $value
279
     * @param null $dateFormat
280
     * @return $this
281
     */
282 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...
283
    {
284
        $params = ['gte' => $value];
285
        if ($dateFormat) {
286
            $params['format'] = $dateFormat;
287
        }
288
        $this->filters[] = ['range' => [$field => $params]];
289
        return $this;
290
    }
291
292
    /**
293
     * добавить фильтр "больше чем"
294
     * @param $field
295
     * @param $value
296
     * @param null $dateFormat
297
     * @return $this
298
     */
299 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...
300
    {
301
        $params = ['gt' => $value];
302
        if ($dateFormat) {
303
            $params['format'] = $dateFormat;
304
        }
305
        $this->filters[] = ['range' => [$field => $params]];
306
        return $this;
307
    }
308
309
    /**
310
     * добавить фильтр "меньше или равно"
311
     * @param $field
312
     * @param $value
313
     * @param null $dateFormat
314
     * @return $this
315
     */
316 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...
317
    {
318
        $params = ['lte' => $value];
319
        if ($dateFormat) {
320
            $params['format'] = $dateFormat;
321
        }
322
        $this->filters[] = ['range' => [$field => $params]];
323
        return $this;
324
    }
325
326
    /**
327
     * добавить фильтр "меньше чем"
328
     * @param $field
329
     * @param $value
330
     * @param null $dateFormat
331
     * @return $this
332
     */
333 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...
334
    {
335
        $params = ['lt' => $value];
336
        if ($dateFormat) {
337
            $params['format'] = $dateFormat;
338
        }
339
        $this->filters[] = ['range' => [$field => $params]];
340
        return $this;
341
    }
342
343
344
    /**
345
     * Добавить фильтр полнотекстового поиска
346
     * этот фильтр влияет на поле релевантности _score.
347
     * Внимание! Класс Query не делает фильтрации или экранирования вводимых значений.
348
     *
349
     * @param $field - поле по которому фильтруем (title, brand.title ...)
350
     * @param $text - поисковая фраза
351
     * @example $q->whereMatch('title', 'Олимпийский чемпион')->addOrderBy('_score', 'desc');
352
     * @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...
353
     */
354
    public function whereMatch($field, $text)
355
    {
356
        $this->filters[] = ['match' => [$field => $text]];
357
        return $this;
358
    }
359
360
361
    /**
362
     * Добавить поле сортировки.
363
     * Для сортировки по релевантности существует псевдополе _score (значение больше - релевантность лучше)
364
     * @param $field - поле сортировки
365
     * @param string $order - направление сортировки asc|desc
366
     * @example $q->addOrderBy('channel', 'asc')->addOrderBy('_score', 'desc');
367
     * @return $this
368
     */
369
    public function addOrderBy($field, $order = 'asc')
370
    {
371
        $field = (string) $field;
372
        $order = (string) $order;
373
        $this->orders[] = [$field => ['order' => $order]];
374
        return $this;
375
    }
376
377
    /**
378
     * Установить лимиты выборки
379
     * @param $limit - сколько строк выбирать
380
     * @param int $offset - сколько строк пропустить
381
     * @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...
382
     */
383
    public function limit($limit, $offset = 0)
384
    {
385
        $this->limit  = (int) $limit;
386
        $this->offset = (int) $offset;
387
        return $this;
388
    }
389
390
391
    /**
392
     * Выполнить запрос к ES и вернуть результаты поиска.
393
     * Внимание! для экономии памяти результаты не хранятся в этом объекте, а сразу возвращаются.
394
     * Чтобы получить кол-во строк всего найденных в индексе (без учета лимита), используй метод getTotalResults()
395
     * @return array - возвращает набор документов
396
     */
397
    public function fetchAll()
398
    {
399
        // build query
400
        $query  = $this->getQuery();
401
402
        // send query to elastic
403
        $start  = microtime(1);
404
        $result = $this->elastic->search($query);
405
        $time   = round((microtime(1) - $start) * 1000); // measure time
406
407
        // extract results
408
        $this->totalResults = 0;
409
        $results = [];
410
        $result = $result['hits'];
411
        $this->totalResults = $result['total']; // total results
412
413
        $context = [
414
            'type'  => 'elastic',
415
            'query' => json_encode($query),
416
            'time'  => $time,
417
            'fetched_rows' => count($result['hits']),
418
            'found_rows'   => $this->totalResults
419
        ];
420
421
        $this->logger->debug("Elastic query ($time ms)", $context);
422
423
        foreach ($result['hits'] as $hit) {
424
            $row = $hit['_source'];
425
            if (isset($hit['fields'])) { // script fields
426
                foreach ($hit['fields'] as $field => $data) {
427
                    if (count($data) == 1) {
428
                        $row[$field] = array_shift($data);
429
                    }
430
                    else {
431
                        $row[$field] = $data;
432
                    }
433
                }
434
            }
435
            $results[] = $row;
436
        }
437
438
        return $results;
439
    }
440
441
    /**
442
     * Выполнить запрос к ES и вернуть первый результат.
443
     * Внимание! для экономии памяти результаты не хранятся в этом объекте, а сразу возвращаются.
444
     * Чтобы получить кол-во строк всего найденных в индексе (без учета лимита), используй метод getTotalResults()
445
     * @return array|null возращает первый найденный документ или null.
446
     */
447
    public function fetchOne()
448
    {
449
        $results = $this->fetchAll();
450
        if (count($results)) {
451
            return array_shift($results);
452
        }
453
        else {
454
            return null;
455
        }
456
    }
457
458
459
    /**
460
     * Количество документов всего найденных в индексе, для последнего запроса.
461
     * @return int
462
     */
463
    public function getTotalResults()
464
    {
465
        return $this->totalResults;
466
    }
467
468
469
    /**
470
     * Собрать запрос
471
     * @return array
472
     */
473
    public function getQuery()
474
    {
475
        $params = [
476
            'index' => $this->index,
477
            'type'  => $this->type,
478
            'body'  => [
479
                'query'   => [
480
                    'bool' => [
481
                        'must' => $this->filters,
482
                    ],
483
                ],
484
            ],
485
            'size'  => $this->limit,
486
            'from'  => $this->offset
487
        ];
488
489
        if ($this->orders) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->orders of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
490
            $params['body']['sort'] = $this->orders;
491
        }
492
493
        if ($this->includes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->includes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
494
            $params['body']['_source']['includes'] = $this->includes;
495
        }
496
497
        if ($this->excludes) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->excludes of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
498
            $params['body']['_source']['excludes'] = $this->excludes;
499
        }
500
501
        if (!isset($params['body']['_source'])) {
502
            $params['body']['_source'] = true;
503
        }
504
505
        if ($this->scriptFields) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->scriptFields of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
506
            $params['body']['script_fields'] = $this->scriptFields;
507
        }
508
509
        return $params;
510
    }
511
512
    public function jsonSerialize() {
513
        return $this->getQuery();
514
    }
515
516
    /**
517
     * Получить JSON-дамп запроса для отладки
518
     * @return string
519
     */
520
    public function getJsonQuery()
521
    {
522
        return json_encode($this);
523
    }
524
}