1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
/* |
4
|
|
|
* This file is part of the ONGR package. |
5
|
|
|
* |
6
|
|
|
* (c) NFQ Technologies UAB <[email protected]> |
7
|
|
|
* |
8
|
|
|
* For the full copyright and license information, please view the LICENSE |
9
|
|
|
* file that was distributed with this source code. |
10
|
|
|
*/ |
11
|
|
|
|
12
|
|
|
namespace ONGR\ElasticsearchBundle\ORM; |
13
|
|
|
|
14
|
|
|
use Elasticsearch\Common\Exceptions\Missing404Exception; |
15
|
|
|
use ONGR\ElasticsearchBundle\Document\DocumentInterface; |
16
|
|
|
use ONGR\ElasticsearchBundle\DSL\Query\TermsQuery; |
17
|
|
|
use ONGR\ElasticsearchBundle\DSL\Search; |
18
|
|
|
use ONGR\ElasticsearchBundle\DSL\Sort\Sort; |
19
|
|
|
use ONGR\ElasticsearchBundle\DSL\Suggester\AbstractSuggester; |
20
|
|
|
use ONGR\ElasticsearchBundle\Result\Converter; |
21
|
|
|
use ONGR\ElasticsearchBundle\Result\DocumentIterator; |
22
|
|
|
use ONGR\ElasticsearchBundle\Result\DocumentScanIterator; |
23
|
|
|
use ONGR\ElasticsearchBundle\Result\IndicesResult; |
24
|
|
|
use ONGR\ElasticsearchBundle\Result\RawResultIterator; |
25
|
|
|
use ONGR\ElasticsearchBundle\Result\RawResultScanIterator; |
26
|
|
|
use ONGR\ElasticsearchBundle\Result\Suggestion\SuggestionIterator; |
27
|
|
|
|
28
|
|
|
/** |
29
|
|
|
* Repository class. |
30
|
|
|
*/ |
31
|
|
|
class Repository |
32
|
|
|
{ |
33
|
|
|
const RESULTS_ARRAY = 'array'; |
34
|
|
|
const RESULTS_OBJECT = 'object'; |
35
|
|
|
const RESULTS_RAW = 'raw'; |
36
|
|
|
const RESULTS_RAW_ITERATOR = 'raw_iterator'; |
37
|
|
|
|
38
|
|
|
/** |
39
|
|
|
* @var Manager |
40
|
|
|
*/ |
41
|
|
|
private $manager; |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* @var array |
45
|
|
|
*/ |
46
|
|
|
private $namespaces = []; |
47
|
|
|
|
48
|
|
|
/** |
49
|
|
|
* @var array |
50
|
|
|
*/ |
51
|
|
|
private $types = []; |
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* @var array |
55
|
|
|
*/ |
56
|
|
|
private $fieldsCache = []; |
57
|
|
|
|
58
|
|
|
/** |
59
|
|
|
* Constructor. |
60
|
|
|
* |
61
|
|
|
* @param Manager $manager |
62
|
|
|
* @param array $repositories |
63
|
|
|
*/ |
64
|
|
|
public function __construct($manager, $repositories) |
65
|
|
|
{ |
66
|
|
|
$this->manager = $manager; |
67
|
|
|
$this->namespaces = $repositories; |
68
|
|
|
$this->types = $this->getTypes(); |
69
|
|
|
} |
70
|
|
|
|
71
|
|
|
/** |
72
|
|
|
* @return array |
73
|
|
|
*/ |
74
|
|
|
public function getTypes() |
75
|
|
|
{ |
76
|
|
|
$types = []; |
77
|
|
|
$meta = $this->getManager()->getBundlesMapping($this->namespaces); |
78
|
|
|
|
79
|
|
|
foreach ($meta as $namespace => $metadata) { |
80
|
|
|
$types[] = $metadata->getType(); |
81
|
|
|
} |
82
|
|
|
|
83
|
|
|
return $types; |
84
|
|
|
} |
85
|
|
|
|
86
|
|
|
/** |
87
|
|
|
* Returns a single document data by ID or null if document is not found. |
88
|
|
|
* |
89
|
|
|
* @param string $id Document Id to find. |
90
|
|
|
* @param string $resultType Result type returned. |
91
|
|
|
* |
92
|
|
|
* @return DocumentInterface|null |
93
|
|
|
* |
94
|
|
|
* @throws \LogicException |
95
|
|
|
*/ |
96
|
|
|
public function find($id, $resultType = self::RESULTS_OBJECT) |
97
|
|
|
{ |
98
|
|
|
if (count($this->types) !== 1) { |
99
|
|
|
throw new \LogicException('Only one type must be specified for the find() method'); |
100
|
|
|
} |
101
|
|
|
|
102
|
|
|
$params = [ |
103
|
|
|
'index' => $this->getManager()->getConnection()->getIndexName(), |
104
|
|
|
'type' => $this->types[0], |
105
|
|
|
'id' => $id, |
106
|
|
|
]; |
107
|
|
|
|
108
|
|
|
try { |
109
|
|
|
$result = $this->getManager()->getConnection()->getClient()->get($params); |
110
|
|
|
} catch (Missing404Exception $e) { |
111
|
|
|
return null; |
112
|
|
|
} |
113
|
|
|
|
114
|
|
|
if ($resultType === self::RESULTS_OBJECT) { |
115
|
|
|
return (new Converter( |
116
|
|
|
$this->getManager()->getTypesMapping(), |
117
|
|
|
$this->getManager()->getBundlesMapping() |
118
|
|
|
))->convertToDocument($result); |
119
|
|
|
} |
120
|
|
|
|
121
|
|
|
return $this->parseResult($result, $resultType, ''); |
122
|
|
|
} |
123
|
|
|
|
124
|
|
|
/** |
125
|
|
|
* Finds entities by a set of criteria. |
126
|
|
|
* |
127
|
|
|
* @param array $criteria Example: ['group' => ['best', 'worst'], 'job' => 'medic']. |
128
|
|
|
* @param array|null $orderBy Example: ['name' => 'ASC', 'surname' => 'DESC']. |
129
|
|
|
* @param int|null $limit Example: 5. |
130
|
|
|
* @param int|null $offset Example: 30. |
131
|
|
|
* @param string $resultType Result type returned. |
132
|
|
|
* |
133
|
|
|
* @return array|DocumentIterator The objects. |
134
|
|
|
*/ |
135
|
|
|
public function findBy( |
136
|
|
|
array $criteria, |
137
|
|
|
array $orderBy = [], |
138
|
|
|
$limit = null, |
139
|
|
|
$offset = null, |
140
|
|
|
$resultType = self::RESULTS_OBJECT |
141
|
|
|
) { |
142
|
|
|
$search = $this->createSearch(); |
143
|
|
|
|
144
|
|
|
if ($limit !== null) { |
145
|
|
|
$search->setSize($limit); |
146
|
|
|
} |
147
|
|
|
if ($offset !== null) { |
148
|
|
|
$search->setFrom($offset); |
149
|
|
|
} |
150
|
|
|
|
151
|
|
View Code Duplication |
foreach ($criteria as $field => $value) { |
|
|
|
|
152
|
|
|
$search->addQuery(new TermsQuery($field, is_array($value) ? $value : [$value]), 'must'); |
153
|
|
|
} |
154
|
|
|
|
155
|
|
View Code Duplication |
foreach ($orderBy as $field => $direction) { |
|
|
|
|
156
|
|
|
$search->addSort(new Sort($field, strcasecmp($direction, 'asc') == 0 ? Sort::ORDER_ASC : Sort::ORDER_DESC)); |
157
|
|
|
} |
158
|
|
|
|
159
|
|
|
return $this->execute($search, $resultType); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
/** |
163
|
|
|
* Finds only one entity by a set of criteria. |
164
|
|
|
* |
165
|
|
|
* @param array $criteria Example: ['group' => ['best', 'worst'], 'job' => 'medic']. |
166
|
|
|
* @param array|null $orderBy Example: ['name' => 'ASC', 'surname' => 'DESC']. |
167
|
|
|
* @param string $resultType Result type returned. |
168
|
|
|
* |
169
|
|
|
* @return DocumentInterface|null The object. |
170
|
|
|
*/ |
171
|
|
|
public function findOneBy(array $criteria, array $orderBy = [], $resultType = self::RESULTS_OBJECT) |
172
|
|
|
{ |
173
|
|
|
$search = $this->createSearch(); |
174
|
|
|
$search->setSize(1); |
175
|
|
|
|
176
|
|
View Code Duplication |
foreach ($criteria as $field => $value) { |
|
|
|
|
177
|
|
|
$search->addQuery(new TermsQuery($field, is_array($value) ? $value : [$value]), 'must'); |
178
|
|
|
} |
179
|
|
|
|
180
|
|
View Code Duplication |
foreach ($orderBy as $field => $direction) { |
|
|
|
|
181
|
|
|
$search->addSort(new Sort($field, strcasecmp($direction, 'asc') == 0 ? Sort::ORDER_ASC : Sort::ORDER_DESC)); |
182
|
|
|
} |
183
|
|
|
|
184
|
|
|
$result = $this |
185
|
|
|
->getManager() |
186
|
|
|
->getConnection() |
187
|
|
|
->search($this->types, $this->checkFields($search->toArray()), $search->getQueryParams()); |
188
|
|
|
|
189
|
|
|
if ($resultType === self::RESULTS_OBJECT) { |
190
|
|
|
$rawData = $result['hits']['hits']; |
191
|
|
|
if (!count($rawData)) { |
192
|
|
|
return null; |
193
|
|
|
} |
194
|
|
|
|
195
|
|
|
return (new Converter( |
196
|
|
|
$this->getManager()->getTypesMapping(), |
197
|
|
|
$this->getManager()->getBundlesMapping() |
198
|
|
|
))->convertToDocument($rawData[0]); |
199
|
|
|
} |
200
|
|
|
|
201
|
|
|
return $this->parseResult($result, $resultType, ''); |
202
|
|
|
} |
203
|
|
|
|
204
|
|
|
/** |
205
|
|
|
* Returns search instance. |
206
|
|
|
* |
207
|
|
|
* @return Search |
208
|
|
|
*/ |
209
|
|
|
public function createSearch() |
210
|
|
|
{ |
211
|
|
|
return new Search(); |
212
|
|
|
} |
213
|
|
|
|
214
|
|
|
/** |
215
|
|
|
* Executes given search. |
216
|
|
|
* |
217
|
|
|
* @param Search $search |
218
|
|
|
* @param string $resultsType |
219
|
|
|
* |
220
|
|
|
* @return DocumentIterator|DocumentScanIterator|RawResultIterator|array |
221
|
|
|
* |
222
|
|
|
* @throws \Exception |
223
|
|
|
*/ |
224
|
|
|
public function execute(Search $search, $resultsType = self::RESULTS_OBJECT) |
225
|
|
|
{ |
226
|
|
|
$results = $this |
227
|
|
|
->getManager() |
228
|
|
|
->getConnection() |
229
|
|
|
->search($this->types, $this->checkFields($search->toArray()), $search->getQueryParams()); |
230
|
|
|
|
231
|
|
|
return $this->parseResult($results, $resultsType, $search->getScroll()); |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Delete by query. |
236
|
|
|
* |
237
|
|
|
* @param Search $search |
238
|
|
|
* |
239
|
|
|
* @return array |
240
|
|
|
*/ |
241
|
|
|
public function deleteByQuery(Search $search) |
242
|
|
|
{ |
243
|
|
|
$results = $this |
244
|
|
|
->getManager() |
245
|
|
|
->getConnection() |
246
|
|
|
->deleteByQuery($this->types, $search->toArray()); |
247
|
|
|
|
248
|
|
|
return new IndicesResult($results); |
249
|
|
|
} |
250
|
|
|
|
251
|
|
|
/** |
252
|
|
|
* Fetches next set of results. |
253
|
|
|
* |
254
|
|
|
* @param string $scrollId |
255
|
|
|
* @param string $scrollDuration |
256
|
|
|
* @param string $resultsType |
257
|
|
|
* |
258
|
|
|
* @return array|DocumentScanIterator |
259
|
|
|
* |
260
|
|
|
* @throws \Exception |
261
|
|
|
*/ |
262
|
|
|
public function scan( |
263
|
|
|
$scrollId, |
264
|
|
|
$scrollDuration = '5m', |
265
|
|
|
$resultsType = self::RESULTS_OBJECT |
266
|
|
|
) { |
267
|
|
|
$results = $this->getManager()->getConnection()->scroll($scrollId, $scrollDuration); |
268
|
|
|
|
269
|
|
|
return $this->parseResult($results, $resultsType, $scrollDuration); |
270
|
|
|
} |
271
|
|
|
|
272
|
|
|
/** |
273
|
|
|
* Get suggestions using suggest api. |
274
|
|
|
* |
275
|
|
|
* @param AbstractSuggester[]|AbstractSuggester $suggesters |
276
|
|
|
* |
277
|
|
|
* @return SuggestionIterator |
278
|
|
|
*/ |
279
|
|
|
public function suggest($suggesters) |
280
|
|
|
{ |
281
|
|
|
if (!is_array($suggesters)) { |
282
|
|
|
$suggesters = [$suggesters]; |
283
|
|
|
} |
284
|
|
|
|
285
|
|
|
$body = []; |
286
|
|
|
/** @var AbstractSuggester $suggester */ |
287
|
|
|
foreach ($suggesters as $suggester) { |
288
|
|
|
$body = array_merge($suggester->toArray(), $body); |
289
|
|
|
} |
290
|
|
|
$results = $this->getManager()->getConnection()->getClient()->suggest(['body' => $body]); |
291
|
|
|
unset($results['_shards']); |
292
|
|
|
|
293
|
|
|
return new SuggestionIterator($results); |
294
|
|
|
} |
295
|
|
|
|
296
|
|
|
/** |
297
|
|
|
* Removes a single document data by ID. |
298
|
|
|
* |
299
|
|
|
* @param string $id Document ID to remove. |
300
|
|
|
* |
301
|
|
|
* @return array |
302
|
|
|
* |
303
|
|
|
* @throws \LogicException |
304
|
|
|
*/ |
305
|
|
|
public function remove($id) |
306
|
|
|
{ |
307
|
|
|
if (count($this->types) == 1) { |
308
|
|
|
$params = [ |
309
|
|
|
'index' => $this->getManager()->getConnection()->getIndexName(), |
310
|
|
|
'type' => $this->types[0], |
311
|
|
|
'id' => $id, |
312
|
|
|
]; |
313
|
|
|
|
314
|
|
|
$response = $this->getManager()->getConnection()->delete($params); |
315
|
|
|
|
316
|
|
|
return $response; |
317
|
|
|
} else { |
318
|
|
|
throw new \LogicException('Only one type must be specified for the find() method'); |
319
|
|
|
} |
320
|
|
|
} |
321
|
|
|
|
322
|
|
|
/** |
323
|
|
|
* Checks if all required fields are added. |
324
|
|
|
* |
325
|
|
|
* @param array $searchArray |
326
|
|
|
* @param array $fields |
327
|
|
|
* |
328
|
|
|
* @return array |
329
|
|
|
*/ |
330
|
|
|
private function checkFields($searchArray, $fields = ['_parent', '_ttl']) |
331
|
|
|
{ |
332
|
|
|
if (empty($fields)) { |
333
|
|
|
return $searchArray; |
334
|
|
|
} |
335
|
|
|
|
336
|
|
|
// Checks if cache is loaded. |
337
|
|
|
if (empty($this->fieldsCache)) { |
338
|
|
|
foreach ($this->getManager()->getBundlesMapping($this->namespaces) as $ns => $properties) { |
339
|
|
|
$this->fieldsCache = array_unique( |
340
|
|
|
array_merge( |
341
|
|
|
$this->fieldsCache, |
342
|
|
|
array_keys($properties->getFields()) |
343
|
|
|
) |
344
|
|
|
); |
345
|
|
|
} |
346
|
|
|
} |
347
|
|
|
|
348
|
|
|
// Adds cached fields to fields array. |
349
|
|
|
foreach (array_intersect($this->fieldsCache, $fields) as $field) { |
350
|
|
|
$searchArray['fields'][] = $field; |
351
|
|
|
} |
352
|
|
|
|
353
|
|
|
// Removes duplicates and checks if its needed to add _source. |
354
|
|
|
if (!empty($searchArray['fields'])) { |
355
|
|
|
$searchArray['fields'] = array_unique($searchArray['fields']); |
356
|
|
|
if (array_diff($searchArray['fields'], $fields) === []) { |
357
|
|
|
$searchArray['fields'][] = '_source'; |
358
|
|
|
} |
359
|
|
|
} |
360
|
|
|
|
361
|
|
|
return $searchArray; |
362
|
|
|
} |
363
|
|
|
|
364
|
|
|
/** |
365
|
|
|
* Parses raw result. |
366
|
|
|
* |
367
|
|
|
* @param array $raw |
368
|
|
|
* @param string $resultsType |
369
|
|
|
* @param string $scrollDuration |
370
|
|
|
* |
371
|
|
|
* @return DocumentIterator|DocumentScanIterator|RawResultIterator|array |
372
|
|
|
* |
373
|
|
|
* @throws \Exception |
374
|
|
|
*/ |
375
|
|
|
private function parseResult($raw, $resultsType, $scrollDuration) |
376
|
|
|
{ |
377
|
|
|
switch ($resultsType) { |
378
|
|
|
case self::RESULTS_OBJECT: |
379
|
|
|
if (isset($raw['_scroll_id'])) { |
380
|
|
|
$iterator = new DocumentScanIterator( |
381
|
|
|
$raw, |
382
|
|
|
$this->getManager()->getTypesMapping(), |
383
|
|
|
$this->getManager()->getBundlesMapping() |
384
|
|
|
); |
385
|
|
|
$iterator |
386
|
|
|
->setRepository($this) |
387
|
|
|
->setScrollDuration($scrollDuration) |
388
|
|
|
->setScrollId($raw['_scroll_id']); |
389
|
|
|
|
390
|
|
|
return $iterator; |
391
|
|
|
} |
392
|
|
|
|
393
|
|
|
return new DocumentIterator( |
394
|
|
|
$raw, |
395
|
|
|
$this->getManager()->getTypesMapping(), |
396
|
|
|
$this->getManager()->getBundlesMapping() |
397
|
|
|
); |
398
|
|
|
case self::RESULTS_ARRAY: |
399
|
|
|
return $this->convertToNormalizedArray($raw); |
400
|
|
|
case self::RESULTS_RAW: |
401
|
|
|
return $raw; |
402
|
|
|
case self::RESULTS_RAW_ITERATOR: |
403
|
|
|
if (isset($raw['_scroll_id'])) { |
404
|
|
|
$iterator = new RawResultScanIterator($raw); |
405
|
|
|
$iterator |
406
|
|
|
->setRepository($this) |
407
|
|
|
->setScrollDuration($scrollDuration) |
408
|
|
|
->setScrollId($raw['_scroll_id']); |
409
|
|
|
|
410
|
|
|
return $iterator; |
411
|
|
|
} |
412
|
|
|
|
413
|
|
|
return new RawResultIterator($raw); |
414
|
|
|
default: |
415
|
|
|
throw new \Exception('Wrong results type selected'); |
416
|
|
|
} |
417
|
|
|
} |
418
|
|
|
|
419
|
|
|
/** |
420
|
|
|
* Normalizes response array. |
421
|
|
|
* |
422
|
|
|
* @param array $data |
423
|
|
|
* |
424
|
|
|
* @return array |
425
|
|
|
*/ |
426
|
|
|
private function convertToNormalizedArray($data) |
427
|
|
|
{ |
428
|
|
|
if (array_key_exists('_source', $data)) { |
429
|
|
|
return $data['_source']; |
430
|
|
|
} |
431
|
|
|
|
432
|
|
|
$output = []; |
433
|
|
|
|
434
|
|
|
if (isset($data['hits']['hits'][0]['_source'])) { |
435
|
|
|
foreach ($data['hits']['hits'] as $item) { |
436
|
|
|
$output[] = $item['_source']; |
437
|
|
|
} |
438
|
|
|
} elseif (isset($data['hits']['hits'][0]['fields'])) { |
439
|
|
|
foreach ($data['hits']['hits'] as $item) { |
440
|
|
|
$output[] = array_map('reset', $item['fields']); |
441
|
|
|
} |
442
|
|
|
} |
443
|
|
|
|
444
|
|
|
return $output; |
445
|
|
|
} |
446
|
|
|
|
447
|
|
|
/** |
448
|
|
|
* Creates new instance of document. |
449
|
|
|
* |
450
|
|
|
* @return DocumentInterface |
451
|
|
|
* |
452
|
|
|
* @throws \LogicException |
453
|
|
|
*/ |
454
|
|
|
public function createDocument() |
455
|
|
|
{ |
456
|
|
|
if (count($this->namespaces) > 1) { |
457
|
|
|
throw new \LogicException( |
458
|
|
|
'Repository can not create new document when it is associated with multiple namespaces' |
459
|
|
|
); |
460
|
|
|
} |
461
|
|
|
|
462
|
|
|
$class = $this->getManager()->getBundlesMapping()[reset($this->namespaces)]->getProxyNamespace(); |
463
|
|
|
|
464
|
|
|
return new $class(); |
465
|
|
|
} |
466
|
|
|
|
467
|
|
|
/** |
468
|
|
|
* Returns elasticsearch manager used in this repository for getting/setting documents. |
469
|
|
|
* |
470
|
|
|
* @return Manager |
471
|
|
|
*/ |
472
|
|
|
public function getManager() |
473
|
|
|
{ |
474
|
|
|
return $this->manager; |
475
|
|
|
} |
476
|
|
|
} |
477
|
|
|
|
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.