Manticore::searchQuery()   F
last analyzed

Complexity

Conditions 17
Paths 3075

Size

Total Lines 130
Code Lines 56

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 17
eloc 56
nc 3075
nop 3
dl 0
loc 130
rs 1.0499
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * Used when the Manticore search daemon is running and Access
5
 *
6
 * @package   ElkArte Forum
7
 * @copyright ElkArte Forum contributors
8
 * @license   BSD http://opensource.org/licenses/BSD-3-Clause (see accompanying LICENSE.txt file)
9
 *
10
 * This file contains code covered by:
11
 * copyright: 2011 Simple Machines (http://www.simplemachines.org)
12
 *
13
 * @version 2.0 Beta 1
14
 *
15
 */
16
17
namespace ElkArte\Search\API;
18
19
use ElkArte\Cache\Cache;
20
use ElkArte\Errors\Errors;
0 ignored issues
show
Bug introduced by
The type ElkArte\Errors\Errors was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
21
use ElkArte\User;
22
use Exception;
23
use mysqli;
24
25
/**
26
 * Manticore API,
27
 *
28
 * What it does:
29
 *
30
 * - Used when the Manticore search daemon is running
31
 * - Access is via Manticore's own implementation of MySQL network protocol
32
 * - Uses the Manticore search engine (https://manticoresearch.com/) which is a drop-in replacement for Sphinx
33
 *
34
 * @package Search
35
 */
36
class Manticore extends AbstractAPI
37
{
38
	/** @var string This is the last version of ElkArte that this was tested on to protect against API changes. */
39
	public $version_compatible = 'ElkArte 2.0 dev';
40
41
	/** @var string This won't work with versions of ElkArte less than this. */
42
	public $min_elk_version = 'ElkArte 2.0 Beta 1';
43
44
	/** @var bool Is it supported?  */
45
	public $is_supported = true;
46
47
	/** @var array What words are banned? */
48
	protected $bannedWords = [];
49
50
	/** @var int What is the minimum word length? */
51
	protected $min_word_length = 4;
52
53
	/** @var array What databases are supported? */
54
	protected $supported_databases = ['MySQL'];
55
56
	/**
57
	 * Nothing to do ...
58
	 */
59
	public function __construct($config, $searchParams)
60
	{
61
		parent::__construct($config, $searchParams);
62
63
		// Is this database supported?
64
		if (!in_array($this->_db->title(), $this->supported_databases, true))
65
		{
66
			$this->is_supported = false;
67
		}
68
	}
69
70
	/**
71
	 * If the settings don't exist, we can't continue.
72
	 */
73
	public function isValid(): bool
74
	{
75
		global $modSettings;
76
77
		return !empty($modSettings['manticore_searchd_server']) && !empty($modSettings['manticore_searchd_port']);
78
	}
79
80
	/**
81
	 * {@inheritDoc}
82
	 */
83
	public function indexedWordQuery(array $words, array $search_data)
84
	{
85
		// Manticore uses its internal engine
86
	}
87
88
	/**
89
	 * {@inheritDoc}
90
	 */
91
	public function supportsExtended(): bool
92
	{
93
		return true;
94
	}
95
96
	/**
97
	 * {@inheritDoc}
98
	 */
99
	public function prepareIndexes($word, &$wordsSearch, &$wordsExclude, $isExcluded, $excludedSubjectWords)
100
	{
101
		$subwords = text2words($word);
102
103
		$fulltextWord = count($subwords) === 1 ? $word : '"' . $word . '"';
104
		$wordsSearch['indexed_words'][] = $fulltextWord;
105
		if ($isExcluded !== false)
106
		{
107
			$wordsExclude[] = $fulltextWord;
108
		}
109
	}
110
111
	/**
112
	 * Wrapper for searchQuery of the SearchAPI
113
	 *
114
	 * @param string[] $search_words
115
	 * @param string[] $excluded_words
116
	 * @param bool[] $participants
117
	 *
118
	 * @return array
119
	 */
120
	public function searchQuery($search_words, $excluded_words, &$participants)
0 ignored issues
show
Unused Code introduced by
The parameter $excluded_words is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

120
	public function searchQuery($search_words, /** @scrutinizer ignore-unused */ $excluded_words, &$participants)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
121
	{
122
		global $context, $modSettings;
123
124
		// Only request the results if they haven't been cached yet.
125
		$cached_results = [];
126
		$cache_key = 'manticore_results_' . md5(User::$info->query_see_board . '_' . $context['params']);
0 ignored issues
show
Bug Best Practice introduced by
The property query_see_board does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
127
		if (!Cache::instance()->getVar($cached_results, $cache_key))
128
		{
129
			// Connect to the manticore searchd and set a few options.
130
			$myManticore = $this->manticoreConnect();
131
132
			// Compile different options for our query
133
			$index = (empty($modSettings['manticore_index_prefix']) ? 'elkarte' : $modSettings['manticore_index_prefix']) . '_index';
134
			$query = 'SELECT *' . (empty($this->_searchParams->topic) ? ', COUNT(*) num' : '') . ', WEIGHT() relevance FROM ' . $index;
0 ignored issues
show
Bug Best Practice introduced by
The property topic does not exist on ElkArte\Search\SearchParams. Since you implemented __get, consider adding a @property annotation.
Loading history...
135
136
			// Construct the (binary mode & |) query.
137
			$where_match = $search_words[0];
138
139
			// Nothing to search, return zero results
140
			if (trim($where_match) === '')
141
			{
142
				return [];
143
			}
144
145
			if ($this->_searchParams->subject_only)
0 ignored issues
show
Bug Best Practice introduced by
The property subject_only does not exist on ElkArte\Search\SearchParams. Since you implemented __get, consider adding a @property annotation.
Loading history...
146
			{
147
				$where_match = '@subject ' . $where_match;
148
			}
149
150
			$query .= " WHERE MATCH('" . $where_match . "')";
151
152
			// Set the limits based on the search parameters, board, member, dates, etc.
153
			$extra_where = $this->buildQueryLimits();
154
			if (!empty($extra_where))
155
			{
156
				$query .= ' AND ' . implode(' AND ', $extra_where);
157
			}
158
159
			// Put together a sort string; besides the main column sort (relevance, id_topic, or num_replies)
160
			$this->_searchParams->sort_dir = strtoupper($this->_searchParams->sort_dir);
0 ignored issues
show
Bug Best Practice introduced by
The property sort_dir does not exist on ElkArte\Search\SearchParams. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
It seems like $this->_searchParams->sort_dir can also be of type null; however, parameter $string of strtoupper() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

160
			$this->_searchParams->sort_dir = strtoupper(/** @scrutinizer ignore-type */ $this->_searchParams->sort_dir);
Loading history...
Bug Best Practice introduced by
The property sort_dir does not exist on ElkArte\Search\SearchParams. Since you implemented __set, consider adding a @property annotation.
Loading history...
161
			$manticore_sort = $this->_searchParams->sort === 'id_msg' ? 'id_topic' : $this->_searchParams->sort;
0 ignored issues
show
Bug Best Practice introduced by
The property sort does not exist on ElkArte\Search\SearchParams. Since you implemented __get, consider adding a @property annotation.
Loading history...
162
163
			// Add secondary sorting based on relevance(rank) value (if not the main sort method) and age
164
			$manticore_sort .= ' ' . $this->_searchParams->sort_dir . ($this->_searchParams->sort === 'relevance' ? '' : ', relevance DESC') . ', poster_time DESC';
165
166
			// Grouping by topic id makes it return only one result per topic, so don't set that for in-topic searches
167
			if (empty($this->_searchParams->topic))
168
			{
169
				// In the topic group, use the most weighty result for display purposes
170
				$query .= ' GROUP BY id_topic WITHIN GROUP ORDER BY relevance DESC';
171
			}
172
173
			$query .= ' ORDER BY ' . $manticore_sort;
174
			$query .= ' LIMIT ' . min(500, $modSettings['manticore_max_results']);
175
176
			// Set any options needed, like field weights.
177
			// ranker is a modification of SPH_RANK_SPH04 sum((4*lcs+2*(min_hit_pos==1)+exact_hit)*user_weight)*1000+bm25
178
			// Each term will return a 0-1000 range we include our acprel value for the final total and order.  Position
179
			// is the relative reply # to a post, so the later a reply in a topic, the less overall weight it is given
180
			// the calculated value of ranker is returned in WEIGHTS() which we name relevance in the query
181
			$subject_weight = empty($modSettings['search_weight_subject']) ? 30 : $modSettings['search_weight_subject'];
182
			$query .= '
183
			OPTION 
184
				field_weights=(subject=' . $subject_weight . ', body=' . (100 - $subject_weight) . '),
185
				ranker=expr(\'sum((4*lcs+2*(min_hit_pos==1)+word_count)*user_weight*position) + acprel + bm25 \'),
186
				idf=plain,
187
				boolean_simplify=1,
188
				max_matches=' . min(500, $modSettings['manticore_max_results']);
189
190
			// Execute the search query.
191
			$request = mysqli_query($myManticore, $query);
192
193
			// Bad query, let's log the error and act like it's not our fault
194
			if ($request === false)
195
			{
196
				// Just log the error.
197
				if (mysqli_error($myManticore) !== '')
198
				{
199
					Errors::instance()->log_error(mysqli_error($myManticore));
200
				}
201
202
				Errors::instance()->fatal_lang_error('error_invalid_search_daemon');
203
			}
204
205
			// Get the relevant information from the search results.
206
			$cached_results = [
207
				'num_results' => 0,
208
				'matches' => [],
209
			];
210
211
			if (mysqli_num_rows($request) !== 0)
0 ignored issues
show
Bug introduced by
It seems like $request can also be of type true; however, parameter $result of mysqli_num_rows() does only seem to accept mysqli_result, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

211
			if (mysqli_num_rows(/** @scrutinizer ignore-type */ $request) !== 0)
Loading history...
212
			{
213
				while ($match = mysqli_fetch_assoc($request))
0 ignored issues
show
Bug introduced by
It seems like $request can also be of type true; however, parameter $result of mysqli_fetch_assoc() does only seem to accept mysqli_result, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

213
				while ($match = mysqli_fetch_assoc(/** @scrutinizer ignore-type */ $request))
Loading history...
214
				{
215
					$num = 0;
216
					if (empty($this->_searchParams->topic))
217
					{
218
						$num = $match['num'] ?? ($match['@count'] ?? 0);
219
					}
220
221
					$cached_results['matches'][$match['id']] = [
222
						'id' => $match['id_topic'],
223
						'num_matches' => $num,
224
						'matches' => [],
225
						'relevance' => round($match['relevance']),
226
					];
227
				}
228
			}
229
230
			mysqli_free_result($request);
0 ignored issues
show
Bug introduced by
It seems like $request can also be of type true; however, parameter $result of mysqli_free_result() does only seem to accept mysqli_result, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

230
			mysqli_free_result(/** @scrutinizer ignore-type */ $request);
Loading history...
231
			mysqli_close($myManticore);
232
233
			$cached_results['num_results'] = count($cached_results['matches']);
234
235
			// Store the search results in the cache.
236
			Cache::instance()->put($cache_key, $cached_results, 600);
237
		}
238
239
		$participants = [];
240
		$topics = [];
241
		foreach (array_slice(array_keys($cached_results['matches']), $this->_req->getRequest('start', 'intval', 0), $modSettings['search_results_per_page']) as $msgID)
242
		{
243
			$topics[$msgID] = $cached_results['matches'][$msgID];
244
			$participants[$cached_results['matches'][$msgID]['id']] = false;
245
		}
246
247
		$this->_num_results = $cached_results['num_results'];
248
249
		return $topics;
250
	}
251
252
	/**
253
	 * Connect to the Manticore server, on failure log error, and exit
254
	 *
255
	 * @return mysqli
256
	 * @throws \ElkArte\Exceptions\Exception
257
	 */
258
	private function manticoreConnect()
259
	{
260
		global $modSettings;
261
262
		set_error_handler(static function () { /* ignore errors */ });
263
		try
264
		{
265
			$myManticore = mysqli_connect(($modSettings['manticore_searchd_server'] === 'localhost' ? '127.0.0.1' : $modSettings['manticore_searchd_server']), '', '', '', (int) $modSettings['manticore_searchd_port']);
0 ignored issues
show
Bug introduced by
The call to mysqli_connect() has too few arguments starting with socket. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

265
			$myManticore = /** @scrutinizer ignore-call */ mysqli_connect(($modSettings['manticore_searchd_server'] === 'localhost' ? '127.0.0.1' : $modSettings['manticore_searchd_server']), '', '', '', (int) $modSettings['manticore_searchd_port']);

This check compares calls to functions or methods with their respective definitions. If the call has less arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
266
		}
267
		catch (Exception)
268
		{
269
			$myManticore = false;
270
		}
271
		finally
272
		{
273
			restore_error_handler();
274
		}
275
276
		// No connection, daemon not running? log the error and exit
277
		if ($myManticore === false)
278
		{
279
			Errors::instance()->fatal_lang_error('error_no_search_daemon');
280
		}
281
282
		return $myManticore;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $myManticore could also return false which is incompatible with the documented return type mysqli. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

Loading history...
283
	}
284
285
	/**
286
	 * {@inheritDoc}
287
	 */
288
	public function useWordIndex()
289
	{
290
		return false;
291
	}
292
293
	/**
294
	 * Builds the query modifiers based on age, member, board, etc
295
	 *
296
	 * @return array
297
	 */
298
	public function buildQueryLimits(): array
299
	{
300
		global $modSettings;
301
302
		$extra_where = [];
303
304
		if (!empty($this->_searchParams->_minMsgID) || !empty($this->_searchParams->_maxMsgID))
305
		{
306
			$extra_where[] = 'id BETWEEN ' . $this->_searchParams->_minMsgID . ' AND ' . (empty($this->_searchParams->_maxMsgID) ? (int) $modSettings['maxMsgID'] : $this->_searchParams->_maxMsgID);
307
		}
308
309
		if (!empty($this->_searchParams->topic))
0 ignored issues
show
Bug Best Practice introduced by
The property topic does not exist on ElkArte\Search\SearchParams. Since you implemented __get, consider adding a @property annotation.
Loading history...
310
		{
311
			$extra_where[] = 'id_topic = ' . (int) $this->_searchParams->topic;
312
		}
313
314
		if (!empty($this->_searchParams->brd))
0 ignored issues
show
Bug Best Practice introduced by
The property brd does not exist on ElkArte\Search\SearchParams. Since you implemented __get, consider adding a @property annotation.
Loading history...
315
		{
316
			$extra_where[] = 'id_board IN (' . implode(',', $this->_searchParams->brd) . ')';
317
		}
318
319
		if (!empty($this->_searchParams->_memberlist))
320
		{
321
			$extra_where[] = 'id_member IN (' . implode(',', $this->_searchParams->_memberlist) . ')';
322
		}
323
324
		return $extra_where;
325
	}
326
}
327