1
|
|
|
<?php |
2
|
|
|
declare(strict_types=1); |
3
|
|
|
|
4
|
|
|
|
5
|
|
|
/** |
6
|
|
|
* FullTextSearch_Elasticsearch - Use Elasticsearch to index the content of your nextcloud |
7
|
|
|
* |
8
|
|
|
* This file is licensed under the Affero General Public License version 3 or |
9
|
|
|
* later. See the COPYING file. |
10
|
|
|
* |
11
|
|
|
* @author Maxence Lange <[email protected]> |
12
|
|
|
* @copyright 2018 |
13
|
|
|
* @license GNU AGPL version 3 or any later version |
14
|
|
|
* |
15
|
|
|
* This program is free software: you can redistribute it and/or modify |
16
|
|
|
* it under the terms of the GNU Affero General Public License as |
17
|
|
|
* published by the Free Software Foundation, either version 3 of the |
18
|
|
|
* License, or (at your option) any later version. |
19
|
|
|
* |
20
|
|
|
* This program is distributed in the hope that it will be useful, |
21
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of |
22
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
23
|
|
|
* GNU Affero General Public License for more details. |
24
|
|
|
* |
25
|
|
|
* You should have received a copy of the GNU Affero General Public License |
26
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. |
27
|
|
|
* |
28
|
|
|
*/ |
29
|
|
|
|
30
|
|
|
|
31
|
|
|
namespace OCA\FullTextSearch_Elasticsearch\Platform; |
32
|
|
|
|
33
|
|
|
|
34
|
|
|
use daita\MySmallPhpTools\Traits\TPathTools; |
35
|
|
|
use Elasticsearch\Client; |
36
|
|
|
use Elasticsearch\ClientBuilder; |
37
|
|
|
use Elasticsearch\Common\Exceptions\BadRequest400Exception; |
38
|
|
|
use Exception; |
39
|
|
|
use OCA\FullTextSearch_Elasticsearch\Exceptions\AccessIsEmptyException; |
40
|
|
|
use OCA\FullTextSearch_Elasticsearch\Exceptions\ConfigurationException; |
41
|
|
|
use OCA\FullTextSearch_Elasticsearch\Service\ConfigService; |
42
|
|
|
use OCA\FullTextSearch_Elasticsearch\Service\IndexService; |
43
|
|
|
use OCA\FullTextSearch_Elasticsearch\Service\MiscService; |
44
|
|
|
use OCA\FullTextSearch_Elasticsearch\Service\SearchService; |
45
|
|
|
use OCP\FullTextSearch\IFullTextSearchPlatform; |
46
|
|
|
use OCP\FullTextSearch\Model\IDocumentAccess; |
47
|
|
|
use OCP\FullTextSearch\Model\IIndex; |
48
|
|
|
use OCP\FullTextSearch\Model\IIndexDocument; |
49
|
|
|
use OCP\FullTextSearch\Model\IRunner; |
50
|
|
|
use OCP\FullTextSearch\Model\ISearchResult; |
51
|
|
|
|
52
|
|
|
|
53
|
|
|
/** |
54
|
|
|
* Class ElasticSearchPlatform |
55
|
|
|
* |
56
|
|
|
* @package OCA\FullTextSearch_Elasticsearch\Platform |
57
|
|
|
*/ |
58
|
|
|
class ElasticSearchPlatform implements IFullTextSearchPlatform { |
59
|
|
|
|
60
|
|
|
|
61
|
|
|
use TPathTools; |
|
|
|
|
62
|
|
|
|
63
|
|
|
|
64
|
|
|
/** @var ConfigService */ |
65
|
|
|
private $configService; |
66
|
|
|
|
67
|
|
|
/** @var IndexService */ |
68
|
|
|
private $indexService; |
69
|
|
|
|
70
|
|
|
/** @var SearchService */ |
71
|
|
|
private $searchService; |
72
|
|
|
|
73
|
|
|
/** @var MiscService */ |
74
|
|
|
private $miscService; |
75
|
|
|
|
76
|
|
|
/** @var Client */ |
77
|
|
|
private $client; |
78
|
|
|
|
79
|
|
|
/** @var IRunner */ |
80
|
|
|
private $runner; |
81
|
|
|
|
82
|
|
|
|
83
|
|
|
/** |
84
|
|
|
* ElasticSearchPlatform constructor. |
85
|
|
|
* |
86
|
|
|
* @param ConfigService $configService |
87
|
|
|
* @param IndexService $indexService |
88
|
|
|
* @param SearchService $searchService |
89
|
|
|
* @param MiscService $miscService |
90
|
|
|
*/ |
91
|
|
|
public function __construct( |
92
|
|
|
ConfigService $configService, IndexService $indexService, SearchService $searchService, |
93
|
|
|
MiscService $miscService |
94
|
|
|
) { |
95
|
|
|
$this->configService = $configService; |
96
|
|
|
$this->indexService = $indexService; |
97
|
|
|
$this->searchService = $searchService; |
98
|
|
|
$this->miscService = $miscService; |
99
|
|
|
} |
100
|
|
|
|
101
|
|
|
|
102
|
|
|
/** |
103
|
|
|
* return a unique Id of the platform. |
104
|
|
|
*/ |
105
|
|
|
public function getId(): string { |
106
|
|
|
return 'elastic_search'; |
107
|
|
|
} |
108
|
|
|
|
109
|
|
|
|
110
|
|
|
/** |
111
|
|
|
* return a unique Id of the platform. |
112
|
|
|
*/ |
113
|
|
|
public function getName(): string { |
114
|
|
|
return 'Elasticsearch'; |
115
|
|
|
} |
116
|
|
|
|
117
|
|
|
|
118
|
|
|
/** |
119
|
|
|
* @return array |
120
|
|
|
* @throws ConfigurationException |
121
|
|
|
*/ |
122
|
|
|
public function getConfiguration(): array { |
123
|
|
|
$result = $this->configService->getConfig(); |
124
|
|
|
|
125
|
|
|
$sanitizedHosts = []; |
126
|
|
|
$hosts = $this->configService->getElasticHost(); |
127
|
|
|
foreach ($hosts as $host) { |
128
|
|
|
$parsedHost = parse_url($host); |
129
|
|
|
$safeHost = $parsedHost['scheme'] . '://'; |
130
|
|
|
if (array_key_exists('user', $parsedHost)) { |
131
|
|
|
$safeHost .= $parsedHost['user'] . ':' . '********' . '@'; |
132
|
|
|
} |
133
|
|
|
$safeHost .= $parsedHost['host']; |
134
|
|
|
$safeHost .= ':' . $parsedHost['port']; |
135
|
|
|
|
136
|
|
|
$sanitizedHosts[] = $safeHost; |
137
|
|
|
} |
138
|
|
|
|
139
|
|
|
$result['elastic_host'] = $sanitizedHosts; |
140
|
|
|
|
141
|
|
|
return $result; |
142
|
|
|
} |
143
|
|
|
|
144
|
|
|
|
145
|
|
|
/** |
146
|
|
|
* @param IRunner $runner |
147
|
|
|
*/ |
148
|
|
|
public function setRunner(IRunner $runner) { |
149
|
|
|
$this->runner = $runner; |
150
|
|
|
} |
151
|
|
|
|
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* Called when loading the platform. |
155
|
|
|
* |
156
|
|
|
* Loading some container and connect to ElasticSearch. |
157
|
|
|
* |
158
|
|
|
* @throws ConfigurationException |
159
|
|
|
* @throws Exception |
160
|
|
|
*/ |
161
|
|
|
public function loadPlatform() { |
162
|
|
|
try { |
163
|
|
|
$this->connectToElastic($this->configService->getElasticHost()); |
164
|
|
|
} catch (ConfigurationException $e) { |
165
|
|
|
throw $e; |
166
|
|
|
} |
167
|
|
|
} |
168
|
|
|
|
169
|
|
|
|
170
|
|
|
/** |
171
|
|
|
* not used yet. |
172
|
|
|
* |
173
|
|
|
* @return bool |
174
|
|
|
*/ |
175
|
|
|
public function testPlatform(): bool { |
176
|
|
|
return $this->client->ping(); |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
|
180
|
|
|
/** |
181
|
|
|
* called before any index |
182
|
|
|
* |
183
|
|
|
* We create a general index. |
184
|
|
|
* |
185
|
|
|
* @throws ConfigurationException |
186
|
|
|
* @throws BadRequest400Exception |
187
|
|
|
*/ |
188
|
|
|
public function initializeIndex() { |
189
|
|
|
$this->indexService->initializeIndex($this->client); |
190
|
|
|
} |
191
|
|
|
|
192
|
|
|
|
193
|
|
|
/** |
194
|
|
|
* resetIndex(); |
195
|
|
|
* |
196
|
|
|
* Called when admin wants to remove an index specific to a $provider. |
197
|
|
|
* $provider can be null, meaning a reset of the whole index. |
198
|
|
|
* |
199
|
|
|
* @param string $providerId |
200
|
|
|
* |
201
|
|
|
* @throws ConfigurationException |
202
|
|
|
*/ |
203
|
|
|
public function resetIndex(string $providerId) { |
204
|
|
|
if ($providerId === 'all') { |
205
|
|
|
$this->indexService->resetIndexAll($this->client); |
206
|
|
|
} else { |
207
|
|
|
$this->indexService->resetIndex($this->client, $providerId); |
208
|
|
|
} |
209
|
|
|
} |
210
|
|
|
|
211
|
|
|
|
212
|
|
|
/** |
213
|
|
|
* @param IIndexDocument $document |
214
|
|
|
* |
215
|
|
|
* @return IIndex |
216
|
|
|
*/ |
217
|
|
|
public function indexDocument(IIndexDocument $document): IIndex { |
218
|
|
|
|
219
|
|
|
$document->initHash(); |
220
|
|
|
|
221
|
|
|
try { |
222
|
|
|
$result = $this->indexService->indexDocument($this->client, $document); |
223
|
|
|
$index = $this->indexService->parseIndexResult($document->getIndex(), $result); |
224
|
|
|
|
225
|
|
|
$this->updateNewIndexResult( |
226
|
|
|
$document->getIndex(), json_encode($result), 'ok', |
227
|
|
|
IRunner::RESULT_TYPE_SUCCESS |
228
|
|
|
); |
229
|
|
|
|
230
|
|
|
return $index; |
231
|
|
|
} catch (Exception $e) { |
232
|
|
|
$this->updateNewIndexResult( |
233
|
|
|
$document->getIndex(), '', 'issue while indexing, testing with empty content', |
234
|
|
|
IRunner::RESULT_TYPE_WARNING |
235
|
|
|
); |
236
|
|
|
|
237
|
|
|
$this->manageIndexErrorException($document, $e); |
238
|
|
|
} |
239
|
|
|
|
240
|
|
|
try { |
241
|
|
|
$result = $this->indexDocumentError($document, $e); |
242
|
|
|
$index = $this->indexService->parseIndexResult($document->getIndex(), $result); |
243
|
|
|
|
244
|
|
|
$this->updateNewIndexResult( |
245
|
|
|
$document->getIndex(), json_encode($result), 'ok', |
246
|
|
|
IRunner::RESULT_TYPE_WARNING |
247
|
|
|
); |
248
|
|
|
|
249
|
|
|
return $index; |
250
|
|
|
} catch (Exception $e) { |
251
|
|
|
$this->updateNewIndexResult( |
252
|
|
|
$document->getIndex(), '', 'fail', |
253
|
|
|
IRunner::RESULT_TYPE_FAIL |
254
|
|
|
); |
255
|
|
|
$this->manageIndexErrorException($document, $e); |
256
|
|
|
} |
257
|
|
|
|
258
|
|
|
return $document->getIndex(); |
259
|
|
|
} |
260
|
|
|
|
261
|
|
|
|
262
|
|
|
/** |
263
|
|
|
* @param IIndexDocument $document |
264
|
|
|
* @param Exception $e |
265
|
|
|
* |
266
|
|
|
* @return array |
267
|
|
|
* @throws AccessIsEmptyException |
268
|
|
|
* @throws ConfigurationException |
269
|
|
|
* @throws Exception |
270
|
|
|
*/ |
271
|
|
|
private function indexDocumentError(IIndexDocument $document, Exception $e): array { |
272
|
|
|
|
273
|
|
|
$this->updateRunnerAction('indexDocumentWithoutContent', true); |
274
|
|
|
|
275
|
|
|
$document->setContent(''); |
276
|
|
|
// $index = $document->getIndex(); |
277
|
|
|
// $index->unsetStatus(Index::INDEX_CONTENT); |
278
|
|
|
|
279
|
|
|
return $this->indexService->indexDocument($this->client, $document); |
280
|
|
|
} |
281
|
|
|
|
282
|
|
|
|
283
|
|
|
/** |
284
|
|
|
* @param IIndexDocument $document |
285
|
|
|
* @param Exception $e |
286
|
|
|
*/ |
287
|
|
|
private function manageIndexErrorException(IIndexDocument $document, Exception $e) { |
288
|
|
|
|
289
|
|
|
$message = $this->parseIndexErrorException($e); |
290
|
|
|
$document->getIndex() |
291
|
|
|
->addError($message, get_class($e), IIndex::ERROR_SEV_3); |
292
|
|
|
$this->updateNewIndexError( |
293
|
|
|
$document->getIndex(), $message, get_class($e), IIndex::ERROR_SEV_3 |
294
|
|
|
); |
295
|
|
|
} |
296
|
|
|
|
297
|
|
|
|
298
|
|
|
/** |
299
|
|
|
* @param Exception $e |
300
|
|
|
* |
301
|
|
|
* @return string |
302
|
|
|
*/ |
303
|
|
|
private function parseIndexErrorException(Exception $e): string { |
304
|
|
|
|
305
|
|
|
$arr = json_decode($e->getMessage(), true); |
306
|
|
|
if (!is_array($arr)) { |
307
|
|
|
return $e->getMessage(); |
308
|
|
|
} |
309
|
|
|
|
310
|
|
|
if (array_key_exists('reason', $arr['error']['root_cause'][0])) { |
311
|
|
|
return $arr['error']['root_cause'][0]['reason']; |
312
|
|
|
} |
313
|
|
|
|
314
|
|
|
return $e->getMessage(); |
315
|
|
|
} |
316
|
|
|
|
317
|
|
|
|
318
|
|
|
/** |
319
|
|
|
* {@inheritdoc} |
320
|
|
|
* @throws ConfigurationException |
321
|
|
|
*/ |
322
|
|
|
public function deleteIndexes(array $indexes) { |
323
|
|
|
try { |
324
|
|
|
$this->indexService->deleteIndexes($this->client, $indexes); |
325
|
|
|
} catch (ConfigurationException $e) { |
326
|
|
|
throw $e; |
327
|
|
|
} |
328
|
|
|
} |
329
|
|
|
|
330
|
|
|
|
331
|
|
|
/** |
332
|
|
|
* {@inheritdoc} |
333
|
|
|
* @throws Exception |
334
|
|
|
*/ |
335
|
|
|
public function searchRequest(ISearchResult $result, IDocumentAccess $access) { |
336
|
|
|
$this->searchService->searchRequest($this->client, $result, $access); |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
|
340
|
|
|
/** |
341
|
|
|
* @param string $providerId |
342
|
|
|
* @param string $documentId |
343
|
|
|
* |
344
|
|
|
* @return IIndexDocument |
345
|
|
|
* @throws ConfigurationException |
346
|
|
|
*/ |
347
|
|
|
public function getDocument(string $providerId, string $documentId): IIndexDocument { |
348
|
|
|
return $this->searchService->getDocument($this->client, $providerId, $documentId); |
349
|
|
|
} |
350
|
|
|
|
351
|
|
|
|
352
|
|
|
private function cleanHost($host) { |
353
|
|
|
return $this->withoutEndSlash($host, false, false); |
354
|
|
|
} |
355
|
|
|
|
356
|
|
|
/** |
357
|
|
|
* @param array $hosts |
358
|
|
|
* |
359
|
|
|
* @throws Exception |
360
|
|
|
*/ |
361
|
|
|
private function connectToElastic(array $hosts) { |
362
|
|
|
|
363
|
|
|
try { |
364
|
|
|
$hosts = array_map([$this, 'cleanHost'], $hosts); |
365
|
|
|
$this->client = ClientBuilder::create() |
366
|
|
|
->setHosts($hosts) |
367
|
|
|
->setRetries(3) |
368
|
|
|
->build(); |
369
|
|
|
|
370
|
|
|
// } |
371
|
|
|
// catch (CouldNotConnectToHost $e) { |
372
|
|
|
// $this 'CouldNotConnectToHost'; |
373
|
|
|
// $previous = $e->getPrevious(); |
374
|
|
|
// if ($previous instanceof MaxRetriesException) { |
375
|
|
|
// echo "Max retries!"; |
376
|
|
|
// } |
377
|
|
|
} catch (Exception $e) { |
378
|
|
|
throw $e; |
379
|
|
|
// echo ' ElasticSearchPlatform::load() Exception --- ' . $e->getMessage() . "\n"; |
380
|
|
|
} |
381
|
|
|
} |
382
|
|
|
|
383
|
|
|
|
384
|
|
|
/** |
385
|
|
|
* @param string $action |
386
|
|
|
* @param bool $force |
387
|
|
|
* |
388
|
|
|
* @throws Exception |
389
|
|
|
*/ |
390
|
|
|
private function updateRunnerAction(string $action, bool $force = false) { |
391
|
|
|
if ($this->runner === null) { |
392
|
|
|
return; |
393
|
|
|
} |
394
|
|
|
|
395
|
|
|
$this->runner->updateAction($action, $force); |
396
|
|
|
} |
397
|
|
|
|
398
|
|
|
|
399
|
|
|
/** |
400
|
|
|
* @param IIndex $index |
401
|
|
|
* @param string $message |
402
|
|
|
* @param string $exception |
403
|
|
|
* @param int $sev |
404
|
|
|
*/ |
405
|
|
|
private function updateNewIndexError(IIndex $index, string $message, string $exception, int $sev |
406
|
|
|
) { |
407
|
|
|
if ($this->runner === null) { |
408
|
|
|
return; |
409
|
|
|
} |
410
|
|
|
|
411
|
|
|
$this->runner->newIndexError($index, $message, $exception, $sev); |
412
|
|
|
} |
413
|
|
|
|
414
|
|
|
|
415
|
|
|
/** |
416
|
|
|
* @param IIndex $index |
417
|
|
|
* @param string $message |
418
|
|
|
* @param string $status |
419
|
|
|
* @param int $type |
420
|
|
|
*/ |
421
|
|
|
private function updateNewIndexResult(IIndex $index, string $message, string $status, int $type |
422
|
|
|
) { |
423
|
|
|
if ($this->runner === null) { |
424
|
|
|
return; |
425
|
|
|
} |
426
|
|
|
|
427
|
|
|
$this->runner->newIndexResult($index, $message, $status, $type); |
428
|
|
|
} |
429
|
|
|
|
430
|
|
|
|
431
|
|
|
} |
432
|
|
|
|
This class, trait or interface has been deprecated. The supplier of the file has supplied an explanatory message.
The explanatory message should give you some clue as to whether and when the type will be removed from the class and what other constant to use instead.