Manticore   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 289
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 97
dl 0
loc 289
rs 9.36
c 1
b 0
f 0
wmc 38

9 Methods

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

121
	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...
122
	{
123
		global $context, $modSettings;
124
125
		// Only request the results if they haven't been cached yet.
126
		$cached_results = [];
127
		$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...
128
		if (!Cache::instance()->getVar($cached_results, $cache_key))
129
		{
130
			// Connect to the manticore searchd and set a few options.
131
			$myManticore = $this->manticoreConnect();
132
133
			// Compile different options for our query
134
			$index = (empty($modSettings['manticore_index_prefix']) ? 'elkarte' : $modSettings['manticore_index_prefix']) . '_index';
135
			$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...
136
137
			// Construct the (binary mode & |) query.
138
			$where_match = $search_words[0];
139
140
			// Nothing to search, return zero results
141
			if (trim($where_match) === '')
142
			{
143
				return [];
144
			}
145
146
			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...
147
			{
148
				$where_match = '@subject ' . $where_match;
149
			}
150
151
			$query .= " WHERE MATCH('" . $where_match . "')";
152
153
			// Set the limits based on the search parameters, board, member, dates, etc
154
			$extra_where = $this->buildQueryLimits();
155
			if (!empty($extra_where))
156
			{
157
				$query .= ' AND ' . implode(' AND ', $extra_where);
158
			}
159
160
			// Put together a sort string; besides the main column sort (relevance, id_topic, or num_replies)
161
			$this->_searchParams->sort_dir = strtoupper($this->_searchParams->sort_dir);
0 ignored issues
show
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

161
			$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...
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...
162
			$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...
163
164
			// Add secondary sorting based on relevance(rank) value (if not the main sort method) and age
165
			$manticore_sort .= ' ' . $this->_searchParams->sort_dir . ($this->_searchParams->sort === 'relevance' ? '' : ', relevance DESC') . ', poster_time DESC';
166
167
			// Grouping by topic id makes it return only one result per topic, so don't set that for in-topic searches
168
			if (empty($this->_searchParams->topic))
169
			{
170
				// In the topic group, use the most weighty result for display purposes
171
				$query .= ' GROUP BY id_topic WITHIN GROUP ORDER BY relevance DESC';
172
			}
173
174
			$query .= ' ORDER BY ' . $manticore_sort;
175
			$query .= ' LIMIT ' . min(500, $modSettings['manticore_max_results']);
176
177
			// Set any options needed, like field weights.
178
			// ranker is a modification of SPH_RANK_SPH04 sum((4*lcs+2*(min_hit_pos==1)+exact_hit)*user_weight)*1000+bm25
179
			// Each term will return a 0-1000 range we include our acprel value for the final total and order.  Position
180
			// is the relative reply # to a post, so the later a reply in a topic the less overall weight it is given
181
			// the calculated value of ranker is returned in WEIGHTS() which we name relevance in the query
182
			$subject_weight = empty($modSettings['search_weight_subject']) ? 30 : $modSettings['search_weight_subject'];
183
			$query .= '
184
			OPTION 
185
				field_weights=(subject=' . $subject_weight . ', body=' . (100 - $subject_weight) . '),
186
				ranker=expr(\'sum((4*lcs+2*(min_hit_pos==1)+word_count)*user_weight*position) + acprel + bm25 \'),
187
				idf=plain,
188
				boolean_simplify=1,
189
				max_matches=' . min(500, $modSettings['manticore_max_results']);
190
191
			// Execute the search query.
192
			$request = mysqli_query($myManticore, $query);
193
194
			// Bad query, lets log the error and act like it's not our fault
195
			if ($request === false)
196
			{
197
				// Just log the error.
198
				if (mysqli_error($myManticore) !== '')
199
				{
200
					Errors::instance()->log_error(mysqli_error($myManticore));
201
				}
202
203
				Errors::instance()->fatal_lang_error('error_invalid_search_daemon');
204
			}
205
206
			// Get the relevant information from the search results.
207
			$cached_results = [
208
				'num_results' => 0,
209
				'matches' => [],
210
			];
211
212
			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

212
			if (mysqli_num_rows(/** @scrutinizer ignore-type */ $request) !== 0)
Loading history...
213
			{
214
				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

214
				while ($match = mysqli_fetch_assoc(/** @scrutinizer ignore-type */ $request))
Loading history...
215
				{
216
					$num = 0;
217
					if (empty($this->_searchParams->topic))
218
					{
219
						$num = $match['num'] ?? ($match['@count'] ?? 0);
220
					}
221
222
					$cached_results['matches'][$match['id']] = [
223
						'id' => $match['id_topic'],
224
						'num_matches' => $num,
225
						'matches' => [],
226
						'relevance' => round($match['relevance']),
227
					];
228
				}
229
			}
230
231
			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

231
			mysqli_free_result(/** @scrutinizer ignore-type */ $request);
Loading history...
232
			mysqli_close($myManticore);
233
234
			$cached_results['num_results'] = count($cached_results['matches']);
235
236
			// Store the search results in the cache.
237
			Cache::instance()->put($cache_key, $cached_results, 600);
238
		}
239
240
		$participants = [];
241
		$topics = [];
242
		foreach (array_slice(array_keys($cached_results['matches']), $this->_req->getRequest('start', 'intval', 0), $modSettings['search_results_per_page']) as $msgID)
243
		{
244
			$topics[$msgID] = $cached_results['matches'][$msgID];
245
			$participants[$cached_results['matches'][$msgID]['id']] = false;
246
		}
247
248
		$this->_num_results = $cached_results['num_results'];
249
250
		return $topics;
251
	}
252
253
	/**
254
	 * Connect to the Manticore server, on failure log error and exit
255
	 *
256
	 * @return mysqli
257
	 * @throws \ElkArte\Exceptions\Exception
258
	 */
259
	private function manticoreConnect()
260
	{
261
		global $modSettings;
262
263
		set_error_handler(static function () { /* ignore errors */ });
264
		try
265
		{
266
			$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

266
			$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...
267
		}
268
		catch (Exception)
269
		{
270
			$myManticore = false;
271
		}
272
		finally
273
		{
274
			restore_error_handler();
275
		}
276
277
		// No connection, daemon not running? log the error and exit
278
		if ($myManticore === false)
279
		{
280
			Errors::instance()->fatal_lang_error('error_no_search_daemon');
281
		}
282
283
		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...
284
	}
285
286
	/**
287
	 * {@inheritDoc}
288
	 */
289
	public function useWordIndex()
290
	{
291
		return false;
292
	}
293
294
	/**
295
	 * Builds the query modifiers based on age, member, board etc
296
	 *
297
	 * @return array
298
	 */
299
	public function buildQueryLimits(): array
300
	{
301
		global $modSettings;
302
303
		$extra_where = [];
304
305
		if (!empty($this->_searchParams->_minMsgID) || !empty($this->_searchParams->_maxMsgID))
306
		{
307
			$extra_where[] = 'id BETWEEN ' . $this->_searchParams->_minMsgID . ' AND ' . (empty($this->_searchParams->_maxMsgID) ? (int) $modSettings['maxMsgID'] : $this->_searchParams->_maxMsgID);
308
		}
309
310
		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...
311
		{
312
			$extra_where[] = 'id_topic = ' . (int) $this->_searchParams->topic;
313
		}
314
315
		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...
316
		{
317
			$extra_where[] = 'id_board IN (' . implode(',', $this->_searchParams->brd) . ')';
318
		}
319
320
		if (!empty($this->_searchParams->_memberlist))
321
		{
322
			$extra_where[] = 'id_member IN (' . implode(',', $this->_searchParams->_memberlist) . ')';
323
		}
324
325
		return $extra_where;
326
	}
327
}
328