Completed
Push — master ( be9b42...2675cf )
by Maxence
02:14
created

ElasticSearchPlatform::searchDocuments()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 5
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
1
<?php
2
/**
3
 * FullTextSearch_ElasticSearch - Use Elasticsearch to index the content of your nextcloud
4
 *
5
 * This file is licensed under the Affero General Public License version 3 or
6
 * later. See the COPYING file.
7
 *
8
 * @author Maxence Lange <[email protected]>
9
 * @copyright 2018
10
 * @license GNU AGPL version 3 or any later version
11
 *
12
 * This program is free software: you can redistribute it and/or modify
13
 * it under the terms of the GNU Affero General Public License as
14
 * published by the Free Software Foundation, either version 3 of the
15
 * License, or (at your option) any later version.
16
 *
17
 * This program is distributed in the hope that it will be useful,
18
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
19
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
20
 * GNU Affero General Public License for more details.
21
 *
22
 * You should have received a copy of the GNU Affero General Public License
23
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
24
 *
25
 */
26
27
namespace OCA\FullTextSearch_ElasticSearch\Platform;
28
29
use Elasticsearch\Client;
30
use Elasticsearch\ClientBuilder;
31
use Elasticsearch\Common\Exceptions\BadRequest400Exception;
32
use Exception;
33
use OCA\FullTextSearch\Exceptions\InterruptException;
34
use OCA\FullTextSearch\Exceptions\TickDoesNotExistException;
35
use OCA\FullTextSearch\IFullTextSearchPlatform;
36
use OCA\FullTextSearch\IFullTextSearchProvider;
37
use OCA\FullTextSearch\Model\DocumentAccess;
38
use OCA\FullTextSearch\Model\Index;
39
use OCA\FullTextSearch\Model\IndexDocument;
40
use OCA\FullTextSearch\Model\Runner;
41
use OCA\FullTextSearch\Model\SearchRequest;
42
use OCA\FullTextSearch_ElasticSearch\AppInfo\Application;
43
use OCA\FullTextSearch_ElasticSearch\Exceptions\AccessIsEmptyException;
44
use OCA\FullTextSearch_ElasticSearch\Exceptions\ConfigurationException;
45
use OCA\FullTextSearch_ElasticSearch\Service\ConfigService;
46
use OCA\FullTextSearch_ElasticSearch\Service\IndexService;
47
use OCA\FullTextSearch_ElasticSearch\Service\MiscService;
48
use OCA\FullTextSearch_ElasticSearch\Service\SearchService;
49
use OCP\AppFramework\QueryException;
50
51
52
class ElasticSearchPlatform implements IFullTextSearchPlatform {
53
54
	/** @var ConfigService */
55
	private $configService;
56
57
	/** @var IndexService */
58
	private $indexService;
59
60
	/** @var SearchService */
61
	private $searchService;
62
63
	/** @var MiscService */
64
	private $miscService;
65
66
	/** @var Client */
67
	private $client;
68
69
	/** @var Runner */
70
	private $runner;
71
72
73
	/**
74
	 * return a unique Id of the platform.
75
	 */
76
	public function getId() {
77
		return 'elastic_search';
78
	}
79
80
	/**
81
	 * return a unique Id of the platform.
82
	 */
83
	public function getName() {
84
		return 'Elasticsearch';
85
	}
86
87
88
	/**
89
	 * @return string
90
	 */
91
	public function getVersion() {
92
		return $this->configService->getAppValue('installed_version');
93
	}
94
95
96
	/**
97
	 * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array|string>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
98
	 * @throws ConfigurationException
99
	 */
100
	public function getConfiguration() {
101
102
		$result = [];
103
		$hosts = $this->configService->getElasticHost();
104
105
		foreach ($hosts as $host) {
106
			$parsedHost = parse_url($host);
107
			$safeHost = $parsedHost['scheme'] . '://';
108
			if (array_key_exists('user', $parsedHost)) {
109
				$safeHost .= $parsedHost['user'] . ':' . '********' . '@';
110
			}
111
			$safeHost .= $parsedHost['host'];
112
			$safeHost .= ':' . $parsedHost['port'];
113
114
			$result[] = $safeHost;
115
		}
116
117
		return [
118
			'elastic_host'  => $result,
119
			'elastic_index' => $this->configService->getElasticIndex()
120
		];
121
	}
122
123
124
	/**
125
	 * @param Runner $runner
126
	 */
127
	public function setRunner(Runner $runner) {
128
		$this->runner = $runner;
129
	}
130
131
	/**
132
	 * @param $action
133
	 * @param bool $force
134
	 *
135
	 * @throws InterruptException
136
	 * @throws TickDoesNotExistException
137
	 */
138
	private function updateRunnerAction($action, $force = false) {
139
		if ($this->runner === null) {
140
			return;
141
		}
142
143
		$this->runner->updateAction($action, $force);
144
	}
145
146
147
	/**
148
	 * @param Index $index
149
	 * @param string $message
150
	 * @param string $exception
151
	 * @param int $sev
152
	 */
153
	private function updateNewIndexError($index, $message, $exception, $sev) {
154
		if ($this->runner === null) {
155
			return;
156
		}
157
158
		$this->runner->newIndexError($index, $message, $exception, $sev);
159
	}
160
161
162
	/**
163
	 * @param Index $index
164
	 * @param string $message
165
	 * @param string $status
166
	 * @param int $type
167
	 */
168
	private function updateNewIndexResult($index, $message, $status, $type) {
169
		if ($this->runner === null) {
170
			return;
171
		}
172
173
		$this->runner->newIndexResult($index, $message, $status, $type);
174
	}
175
176
177
	/**
178
	 * Called when loading the platform.
179
	 *
180
	 * Loading some container and connect to ElasticSearch.
181
	 *
182
	 * @throws ConfigurationException
183
	 * @throws QueryException
184
	 * @throws Exception
185
	 */
186
	public function loadPlatform() {
187
		$app = new Application();
188
189
		$container = $app->getContainer();
190
		$this->configService = $container->query(ConfigService::class);
191
		$this->indexService = $container->query(IndexService::class);
192
		$this->searchService = $container->query(SearchService::class);
193
		$this->miscService = $container->query(MiscService::class);
194
195
		try {
196
			$this->connectToElastic($this->configService->getElasticHost());
197
		} catch (ConfigurationException $e) {
198
			throw $e;
199
		}
200
	}
201
202
203
	/**
204
	 * not used yet.
205
	 *
206
	 * @return bool
207
	 */
208
	public function testPlatform() {
209
		return $this->client->ping();
210
	}
211
212
213
	/**
214
	 * called before any index
215
	 *
216
	 * We create a general index.
217
	 *
218
	 * @throws ConfigurationException
219
	 * @throws BadRequest400Exception
220
	 */
221
	public function initializeIndex() {
222
		$this->indexService->initializeIndex($this->client);
223
	}
224
225
226
	/**
227
	 * resetIndex();
228
	 *
229
	 * Called when admin wants to remove an index specific to a $provider.
230
	 * $provider can be null, meaning a reset of the whole index.
231
	 *
232
	 * @param string $providerId
233
	 *
234
	 * @throws ConfigurationException
235
	 */
236
	public function resetIndex($providerId) {
237
		if ($providerId === 'all') {
238
			$this->indexService->resetIndexAll($this->client);
239
		} else {
240
			$this->indexService->resetIndex($this->client, $providerId);
241
		}
242
	}
243
244
245
	/**
246
	 * @deprecated
247
	 *
248
	 * @param IFullTextSearchProvider $provider
249
	 * @param $documents
250
	 */
251
	public function indexDocuments(IFullTextSearchProvider $provider, $documents) {
252
253
	}
254
255
256
	/**
257
	 * @param IFullTextSearchProvider $provider
258
	 * @param IndexDocument $document
259
	 *
260
	 * @return Index
261
	 * @throws AccessIsEmptyException
262
	 * @throws ConfigurationException
263
	 * @throws InterruptException
264
	 * @throws TickDoesNotExistException
265
	 */
266
	public function indexDocument(IFullTextSearchProvider $provider, IndexDocument $document) {
267
268
		$document->initHash();
269
270
		try {
271
			$result = $this->indexService->indexDocument($this->client, $provider, $document);
272
273
			$index = $this->indexService->parseIndexResult($document->getIndex(), $result);
0 ignored issues
show
Documentation introduced by
$result is of type callable, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
274
275
			$this->updateNewIndexResult(
276
				$document->getIndex(), json_encode($result), 'ok',
277
				Runner::RESULT_TYPE_SUCCESS
278
			);
279
280
			return $index;
281
		} catch (Exception $e) {
282
			$this->updateNewIndexResult(
283
				$document->getIndex(), '', 'issue while indexing, testing with empty content',
284
				Runner::RESULT_TYPE_WARNING
285
			);
286
287
			$this->manageIndexErrorException($document, $e);
288
		}
289
290
		try {
291
			$result = $this->indexDocumentError($provider, $document, $e);
292
			$index = $this->indexService->parseIndexResult($document->getIndex(), $result);
0 ignored issues
show
Documentation introduced by
$result is of type callable, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
293
294
			$this->updateNewIndexResult(
295
				$document->getIndex(), json_encode($result), 'ok',
296
				Runner::RESULT_TYPE_WARNING
297
			);
298
299
			return $index;
300
		} catch (Exception $e) {
301
			$this->updateNewIndexResult(
302
				$document->getIndex(), '', 'fail',
303
				Runner::RESULT_TYPE_FAIL
304
			);
305
			$this->manageIndexErrorException($document, $e);
306
		}
307
308
		return $document->getIndex();
309
	}
310
311
312
	/**
313
	 * @param IFullTextSearchProvider $provider
314
	 * @param IndexDocument $document
315
	 * @param Exception $e
316
	 *
317
	 * @return array
0 ignored issues
show
Documentation introduced by
Should the return type not be callable? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
318
	 * @throws AccessIsEmptyException
319
	 * @throws ConfigurationException
320
	 * @throws InterruptException
321
	 * @throws TickDoesNotExistException
322
	 */
323
	private function indexDocumentError(
324
		IFullTextSearchProvider $provider, IndexDocument $document, Exception $e
325
	) {
326
327
		$this->updateRunnerAction('indexDocumentWithoutContent', true);
328
329
		$document->setContent('');
330
//		$index = $document->getIndex();
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
331
//		$index->unsetStatus(Index::INDEX_CONTENT);
332
333
		$result = $this->indexService->indexDocument($this->client, $provider, $document);
334
335
		return $result;
336
	}
337
338
339
	/**
340
	 * @param IndexDocument $document
341
	 * @param Exception $e
342
	 */
343
	private function manageIndexErrorException(IndexDocument $document, Exception $e) {
344
345
		$message = $this->parseIndexErrorException($e);
346
		$document->getIndex()
347
				 ->addError($message, get_class($e), Index::ERROR_SEV_3);
348
		$this->updateNewIndexError(
349
			$document->getIndex(), $message, get_class($e), Index::ERROR_SEV_3
350
		);
351
	}
352
353
354
	/**
355
	 * @param Exception $e
356
	 *
357
	 * @return string
358
	 */
359
	private function parseIndexErrorException(Exception $e) {
360
361
		$arr = json_decode($e->getMessage(), true);
362
		if (!is_array($arr)) {
363
			return $e->getMessage();
364
		}
365
366
		if (array_key_exists('reason', $arr['error']['root_cause'][0])) {
367
			return $arr['error']['root_cause'][0]['reason'];
368
		}
369
370
		return $e->getMessage();
371
	}
372
373
374
	/**
375
	 * {@inheritdoc}
376
	 * @throws ConfigurationException
377
	 */
378
	public function deleteIndexes($indexes) {
379
		try {
380
			$this->indexService->deleteIndexes($this->client, $indexes);
381
		} catch (ConfigurationException $e) {
382
			throw $e;
383
		}
384
	}
385
386
387
	/**
388
	 * {@inheritdoc}
389
	 * @throws ConfigurationException
390
	 * @throws Exception
391
	 */
392
	public function searchDocuments(
393
		IFullTextSearchProvider $provider, DocumentAccess $access, SearchRequest $request
394
	) {
395
		return $this->searchService->searchDocuments($this->client, $provider, $access, $request);
396
	}
397
398
399
	/**
400
	 * @param string $providerId
401
	 * @param string $documentId
402
	 *
403
	 * @return IndexDocument
404
	 * @throws ConfigurationException
405
	 */
406
	public function getDocument($providerId, $documentId) {
407
		return $this->searchService->getDocument($this->client, $providerId, $documentId);
408
	}
409
410
411
	/**
412
	 * @param array $hosts
413
	 *
414
	 * @throws Exception
415
	 */
416
	private function connectToElastic($hosts) {
417
418
		try {
419
			$hosts = array_map([MiscService::class, 'noEndSlash'], $hosts);
420
			$this->client = ClientBuilder::create()
421
										 ->setHosts($hosts)
422
										 ->setRetries(3)
423
										 ->build();
424
425
//		}
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
426
//		catch (CouldNotConnectToHost $e) {
427
//			$this 'CouldNotConnectToHost';
428
//			$previous = $e->getPrevious();
429
//			if ($previous instanceof MaxRetriesException) {
430
//				echo "Max retries!";
431
//			}
432
		} catch (Exception $e) {
433
			throw $e;
434
//			echo ' ElasticSearchPlatform::load() Exception --- ' . $e->getMessage() . "\n";
0 ignored issues
show
Unused Code Comprehensibility introduced by
48% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
435
		}
436
	}
437
438
439
}