Completed
Push — devel ( 9422b1...d1cca5 )
by Alexey
01:53
created

Query::fetchRaw()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 8.8571
c 0
b 0
f 0
cc 1
eloc 17
nc 1
nop 0
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
        if ( is_array($field) ) {
357
            $this->filters[] = [
358
                'multi_match' => [
359
                    'query'  => $text,
360
                    'fields' => $field
361
                ]
362
            ];
363
        }
364
        else {
365
            $this->filters[] = ['match' => [$field => $text]];
366
        }
367
        return $this;
368
    }
369
370
371
    /**
372
     * Добавить поле сортировки.
373
     * Для сортировки по релевантности существует псевдополе _score (значение больше - релевантность лучше)
374
     * @param $field - поле сортировки
375
     * @param string $order - направление сортировки asc|desc
376
     * @example $q->addOrderBy('channel', 'asc')->addOrderBy('_score', 'desc');
377
     * @return $this
378
     */
379
    public function addOrderBy($field, $order = 'asc')
380
    {
381
        $field = (string) $field;
382
        $order = (string) $order;
383
        $this->orders[] = [$field => ['order' => $order]];
384
        return $this;
385
    }
386
387
    /**
388
     * Установить лимиты выборки
389
     * @param $limit - сколько строк выбирать
390
     * @param int $offset - сколько строк пропустить
391
     * @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...
392
     */
393
    public function limit($limit, $offset = 0)
394
    {
395
        $this->limit  = (int) $limit;
396
        $this->offset = (int) $offset;
397
        return $this;
398
    }
399
400
401
    public function fetchRaw()
402
    {
403
        $this->totalResults = 0;
404
405
        // build query
406
        $query  = $this->getQuery();
407
408
        // send query to elastic
409
        $start  = microtime(1);
410
411
        $result = $this->elastic->search($query);
412
413
        // measure time
414
        $time   = round((microtime(1) - $start) * 1000);
415
416
        // total results
417
        $this->totalResults = $result['hits']['total'];
418
419
        // log
420
        $index = $this->index . '/' . $this->type;
421
        $context = [
422
            'type'  => 'elastic',
423
            'query' => json_encode($query),
424
            'time'  => $time,
425
            'index' => $index,
426
            'found_rows'   => $this->totalResults,
427
            'fetched_rows' => count($result['hits']['hits'])
428
        ];
429
430
        $this->logger->debug("Elastic query (index: $index, time: $time ms)", $context);
431
432
        return $result;
433
    }
434
435
436
    /**
437
     * Выполнить запрос к ES и вернуть результаты поиска.
438
     * Внимание! для экономии памяти результаты не хранятся в этом объекте, а сразу возвращаются.
439
     * Чтобы получить кол-во строк всего найденных в индексе (без учета лимита), используй метод getTotalResults()
440
     * @return array - возвращает набор документов
441
     */
442
    public function fetchAll()
443
    {
444
        $result = $this->fetchRaw();
445
446
        $results = [];
447
        foreach ($result['hits']['hits'] as $hit) {
448
            $row = $hit['_source'];
449
            if (isset($hit['fields'])) { // script fields
450
                foreach ($hit['fields'] as $field => $data) {
451
                    if (count($data) == 1) {
452
                        $row[$field] = array_shift($data);
453
                    }
454
                    else {
455
                        $row[$field] = $data;
456
                    }
457
                }
458
            }
459
            $results[] = $row;
460
        }
461
462
        return $results;
463
    }
464
465
    /**
466
     * Выполнить запрос к ES и вернуть первый результат.
467
     * Внимание! для экономии памяти результаты не хранятся в этом объекте, а сразу возвращаются.
468
     * Чтобы получить кол-во строк всего найденных в индексе (без учета лимита), используй метод getTotalResults()
469
     * @return array|null возращает первый найденный документ или null.
470
     */
471
    public function fetchOne()
472
    {
473
        $results = $this->fetchAll();
474
        if (count($results)) {
475
            return array_shift($results);
476
        }
477
        else {
478
            return null;
479
        }
480
    }
481
482
483
    /**
484
     * Количество документов всего найденных в индексе, для последнего запроса.
485
     * @return int
486
     */
487
    public function getTotalResults()
488
    {
489
        return $this->totalResults;
490
    }
491
492
493
    /**
494
     * Собрать запрос
495
     * @return array
496
     */
497
    public function getQuery()
498
    {
499
        $params = [
500
            'index' => $this->index,
501
            'type'  => $this->type,
502
            'body'  => [
503
                'query'   => [
504
                    'bool' => [
505
                        'must' => $this->filters,
506
                    ],
507
                ],
508
            ],
509
            'size'  => $this->limit,
510
            'from'  => $this->offset
511
        ];
512
513
        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...
514
            $params['body']['sort'] = $this->orders;
515
        }
516
517
        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...
518
            $params['body']['_source']['includes'] = $this->includes;
519
        }
520
521
        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...
522
            $params['body']['_source']['excludes'] = $this->excludes;
523
        }
524
525
        if (!isset($params['body']['_source'])) {
526
            $params['body']['_source'] = true;
527
        }
528
529
        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...
530
            $params['body']['script_fields'] = $this->scriptFields;
531
        }
532
533
        return $params;
534
    }
535
536
    public function jsonSerialize() {
537
        return $this->getQuery();
538
    }
539
540
    /**
541
     * Получить JSON-дамп запроса для отладки
542
     * @return string
543
     */
544
    public function getJsonQuery()
545
    {
546
        return json_encode($this);
547
    }
548
}