Standard::getResults()   F
last analyzed

Complexity

Conditions 41
Paths > 20000

Size

Total Lines 209
Code Lines 114

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 61
CRAP Score 105.3364

Importance

Changes 0
Metric Value
cc 41
eloc 114
nc 90945
nop 1
dl 0
loc 209
ccs 61
cts 92
cp 0.663
crap 105.3364
rs 0
c 0
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
 * Standard non-full index, non-custom index search
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\Search\Cache\Session;
20
use Exception;
21
22
/**
23
 * SearchAPI-Standard.class.php, Standard non-full index, non-custom index search
24
 *
25
 * @package Search
26
 */
27
class Standard extends AbstractAPI
28
{
29
	/** @var string This is the last version of ElkArte that this was tested on, to protect against API changes */
30
	public $version_compatible = 'ElkArte 2.0 dev';
31
32
	/** @var string This won't work with versions of ElkArte less than this. */
33
	public $min_elk_version = 'ElkArte 2.0 Beta 1';
34
35
	/** @var bool Standard search is supported by default. */
36
	public $is_supported = true;
37
38
	/** @var object */
39
	protected $_search_cache;
40
41
	/** @var int total count of search results */
42
	protected $_num_results;
43
44
	/**
45
	 * Wrapper for searchQuery of the SearchAPI
46
	 *
47
	 * @param string[] $search_words
48
	 * @param string[] $excluded_words
49
	 * @param bool[] $participants
50
	 *
51
	 * @return array
52
	 * @throws Exception
53
	 */
54
	public function searchQuery($search_words, $excluded_words, &$participants): array
55
	{
56
		global $context, $modSettings;
57
58
		$this->_search_cache = new Session();
59
		$this->_searchWords = $search_words;
60
		$search_id = 0;
61
62
		if ($this->_search_cache->existsWithParams($context['params']) === false)
63
		{
64
			$search_id = $this->_search_cache->increaseId($modSettings['search_pointer'] ?? 0);
65
66
			// Store the new id right off.
67
			updateSettings([
68
				'search_pointer' => $search_id
69
			]);
70
71
			// Clear the previous cache of the final results cache.
72 2
			$this->clearCacheResults($search_id);
73
74 2
			if ($this->_searchParams['subject_only'])
75
			{
76 2
				$num_res = $this->getSubjectResults($search_id, $search_words, $excluded_words);
77 2
			}
78 2
			else
79
			{
80 2
				$num_res = $this->getResults($search_id);
81
82 2
				if (empty($num_res))
83
				{
84 2
					throw new Exception('query_not_specific_enough');
85 2
				}
86
			}
87
88
			$this->_search_cache->setNumResults($num_res);
89 2
		}
90
91 2
		// *** Retrieve the results to be shown on the page
92
		$topics = [];
93
		$participants = $this->addRelevance($topics, $search_id, $this->_req->getRequest('start', 'intval', 0), $modSettings['search_results_per_page']);
94
		$this->_num_results = $this->_search_cache->getNumResults();
95
96
		return $topics;
97
	}
98
99
	/**
100 2
	 * Delete logs of previous searches
101 2
	 *
102
	 * @param int $id_search - the id of the search to delete from logs
103 2
	 */
104
	public function clearCacheResults($id_search): void
105
	{
106
		$this->_db_search->search_query('', '
107
			DELETE FROM {db_prefix}log_search_results
108
			WHERE id_search = {int:search_id}',
109 2
			[
110
				'search_id' => $id_search,
111
			]
112 2
		);
113
	}
114 2
115 2
	/**
116 1
	 * Grabs results when the search is performed only within the subject
117 2
	 *
118 2
	 * @param int $id_search - the id of the search
119
	 *
120 2
	 * @return int - number of results otherwise
121
	 */
122 2
	protected function getSubjectResults($id_search, $search_words, $excluded_words): int
123
	{
124
		global $modSettings;
125
126
		$numSubjectResults = 0;
127
128
		// We do this to try and avoid duplicate keys on databases not supporting INSERT IGNORE.
129
		foreach ($search_words as $words)
130 2
		{
131
			$subject_query_params = [];
132 2
			$subject_query = [
133
				'from' => '{db_prefix}topics AS t',
134
				'inner_join' => [],
135
				'left_join' => [],
136 2
				'where' => [],
137
			];
138
139 2
			if ($modSettings['postmod_active'])
140
			{
141
				$subject_query['where'][] = 't.approved = {int:is_approved}';
142
			}
143
144
			$numTables = 0;
145
			$prev_join = 0;
146
			$numSubjectResults = 0;
147
			foreach ($words['subject_words'] as $subjectWord)
148
			{
149
				$numTables++;
150
				if (in_array($subjectWord, $excluded_words, true))
151
				{
152
					$subject_query['left_join'][] = '{db_prefix}log_search_subjects AS subj' . $numTables . ' ON (subj' . $numTables . '.word ' . (empty($modSettings['search_match_words']) ? '{ilike} {string:subject_words_' . $numTables . '_wild}' : '= {string:subject_words_' . $numTables . '}') . ' AND subj' . $numTables . '.id_topic = t.id_topic)';
153
					$subject_query['where'][] = '(subj' . $numTables . '.word IS NULL)';
154
				}
155
				else
156
				{
157
					$subject_query['inner_join'][] = '{db_prefix}log_search_subjects AS subj' . $numTables . ' ON (subj' . $numTables . '.id_topic = ' . ($prev_join === 0 ? 't' : 'subj' . $prev_join) . '.id_topic)';
158
					$subject_query['where'][] = 'subj' . $numTables . '.word ' . (empty($modSettings['search_match_words']) ? '{ilike} {string:subject_words_' . $numTables . '_wild}' : '= {string:subject_words_' . $numTables . '}');
159
					$prev_join = $numTables;
160
				}
161
162
				$subject_query_params['subject_words_' . $numTables] = $subjectWord;
163
				$subject_query_params['subject_words_' . $numTables . '_wild'] = '%' . $subjectWord . '%';
164
			}
165
166
			if (!empty($this->_searchParams->_userQuery))
167
			{
168
				$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_topic = t.id_topic)';
169
				$subject_query['where'][] = $this->_searchParams->_userQuery;
170
			}
171
172
			if (!empty($this->_searchParams['topic']))
173
			{
174
				$subject_query['where'][] = 't.id_topic = ' . $this->_searchParams['topic'];
175
			}
176
177
			if (!empty($this->_searchParams->_minMsgID))
178
			{
179
				$subject_query['where'][] = 't.id_first_msg >= ' . $this->_searchParams->_minMsgID;
180
			}
181
182
			if (!empty($this->_searchParams->_maxMsgID))
183
			{
184
				$subject_query['where'][] = 't.id_last_msg <= ' . $this->_searchParams->_maxMsgID;
185
			}
186
187
			if (!empty($this->_searchParams->_boardQuery))
188
			{
189
				$subject_query['where'][] = 't.id_board ' . $this->_searchParams->_boardQuery;
190
			}
191
192
			if (!empty($this->_excludedPhrases))
193
			{
194
				$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
195
196
				$count = 0;
197
				foreach ($this->_excludedPhrases as $phrase)
198
				{
199
					$subject_query['where'][] = 'm.subject ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? '{not_ilike}' : '{not_rlike}') . ' {string:excluded_phrases_' . $count . '}';
200
					$subject_query_params['excluded_phrases_' . ($count++)] = $this->prepareWord($phrase, $this->noRegexp());
201
				}
202
			}
203
204
			// Build the search query
205
			$subject_query['select'] = [
206
				'id_search' => '{int:id_search}',
207
				'id_topic' => 't.id_topic',
208
				'relevance' => $this->_build_relevance(),
209
				'id_msg' => empty($this->_searchParams->_userQuery) ? 't.id_first_msg' : 'm.id_msg',
210
				'num_matches' => 1,
211
			];
212
213
			$subject_query['parameters'] = array_merge($subject_query_params, [
214
				'id_search' => $id_search,
215
				'min_msg' => $this->_searchParams->_minMsg,
216
				'recent_message' => $this->_searchParams->_recentMsg,
217
				'huge_topic_posts' => $this->config->humungousTopicPosts,
0 ignored issues
show
Bug Best Practice introduced by
The property humungousTopicPosts does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
218
				'short_topic_posts' => $this->config->shortTopicPosts,
0 ignored issues
show
Bug Best Practice introduced by
The property shortTopicPosts does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
219
				'is_approved' => 1,
220
				'limit' => empty($modSettings['search_max_results']) ? 0 : $modSettings['search_max_results'] - $numSubjectResults,
221
			]);
222
223
			call_integration_hook('integrate_subject_only_search_query', [&$subject_query, &$subject_query_params]);
224
225
			$numSubjectResults += $this->_build_search_results_log($subject_query);
226
			if (empty($modSettings['search_max_results']))
227
			{
228
				continue;
229
			}
230
231
			if ($numSubjectResults < $modSettings['search_max_results'])
232
			{
233
				continue;
234
			}
235
236
			break;
237
		}
238
239
		return $numSubjectResults;
240
	}
241
242
	/**
243
	 * If the query uses regexp or not
244
	 *
245
	 * @return bool
246
	 */
247
	protected function noRegexp(): bool
248
	{
249
		return $this->_searchArray->getNoRegexp();
250
	}
251
252
	/**
253
	 * Build the search relevance query
254
	 *
255
	 * @param null|array $factors - if factors are specified, that array will
256
	 * be used to build the relevance value, otherwise the function will use
257
	 * $this->_weight_factors
258
	 *
259
	 * @return string
260
	 */
261
	private function _build_relevance($factors = null): string
262
	{
263
		$relevance = '1000 * (';
264
265 2
		if (is_array($factors))
266
		{
267 2
			$weight_total = 0;
268
			foreach ($factors as $type => $value)
269
			{
270
				$relevance .= $this->_weight[$type];
271
				if (!empty($value['search']))
272
				{
273
					$relevance .= ' * ' . $value['search'];
274
				}
275
276
				$relevance .= ' + ';
277
				$weight_total += $this->_weight[$type];
278
			}
279 2
		}
280
		else
281 2
		{
282
			$weight_total = $this->_weight_total;
283 2
			foreach ($this->_weight_factors as $type => $value)
284
			{
285 2
				if (isset($value['results']))
286 2
				{
287
					$relevance .= $this->_weight[$type];
288 2
					if (!empty($value['results']))
289 2
					{
290
						$relevance .= ' * ' . $value['results'];
291 2
					}
292
293
					$relevance .= ' + ';
294 2
				}
295 2
			}
296
		}
297
298
		return substr($relevance, 0, -3) . ') / ' . $weight_total . ' AS relevance';
299
	}
300 2
301 2
	/**
302
	 * Inserts the data into log_search_results
303 2
	 *
304
	 * @param array $main_query - An array holding all the query parts.
305 2
	 *   Structure:
306 2
	 *        'select' => string[] - the select columns
307
	 *        'from' => string - the table for the FROM clause
308 2
	 *        'inner_join' => string[] - any INNER JOIN
309
	 *        'left_join' => string[] - any LEFT JOIN
310
	 *        'where' => string[] - the conditions
311 2
	 *        'group_by' => string[] - the fields to group by
312
	 *        'parameters' => mixed[] - any parameter required by the query
313
	 * @param string $query_identifier - a string to identify the query
314
	 * @param bool $use_old_ids - if true, the topic ids retrieved by a previous
315
	 * call to this function will be used to identify duplicates
316 2
	 *
317
	 * @return int - the number of rows affected by the query
318 2
	 */
319
	private function _build_search_results_log($main_query, $query_identifier = '', $use_old_ids = false): int
320
	{
321
		static $usedIDs;
322
323
		$ignoreRequest = $this->_db_search->search_query($query_identifier, ($this->_db->support_ignore() ? ('
324
			INSERT IGNORE INTO {db_prefix}log_search_results
325
				(' . implode(', ', array_keys($main_query['select'])) . ')') : '') . '
326
			SELECT
327
				' . implode(', ', $main_query['select']) . '
328
			FROM ' . $main_query['from'] . (empty($main_query['inner_join']) ? '' : '
329
				INNER JOIN ' . implode('
330
				INNER JOIN ', array_unique($main_query['inner_join']))) . (empty($main_query['left_join']) ? '' : '
331
				LEFT JOIN ' . implode('
332
				LEFT JOIN ', array_unique($main_query['left_join']))) . (empty($main_query['where']) ? '' : '
333
			WHERE ') . implode('
334
				AND ', array_unique($main_query['where'])) . (empty($main_query['group_by']) ? '' : '
335
			GROUP BY ' . implode(', ', array_unique($main_query['group_by']))) . (empty($main_query['parameters']['limit']) ? '' : '
336
			LIMIT {int:limit}'),
337
			$main_query['parameters']
338
		);
339 2
340
		// If the database doesn't support IGNORE to make this fast, we need to do some tracking.
341 2
		if (!$this->_db->support_ignore())
342
		{
343 2
			$inserts = [];
344
			while (($row = $ignoreRequest->fetch_assoc()))
345 2
			{
346
				// No duplicates!
347 2
				if ($use_old_ids)
348 2
				{
349 2
					if (isset($usedIDs[$row['id_topic']]))
350 2
					{
351 2
						continue;
352 2
					}
353 2
				}
354 2
				elseif (isset($inserts[$row['id_topic']]))
355 2
				{
356 2
					continue;
357 2
				}
358 2
359
				$usedIDs[$row['id_topic']] = true;
360
				foreach ($row as $key => $value)
361
				{
362 2
					$inserts[$row['id_topic']][] = (int) $value;
363
				}
364 1
			}
365
			$ignoreRequest->free_result();
366 1
367
			// Now put them in!
368
			if (!empty($inserts))
369 1
			{
370
				$query_columns = array_map(static function () {
371 1
					return 'int';
372
				}, $main_query['select']);
373 1
374
				$this->_db->insert('',
375
					'{db_prefix}log_search_results',
376 1
					$query_columns,
377
					$inserts,
378
					['id_search', 'id_topic']
379
				);
380
			}
381 1
382 1
			return count($inserts);
383
		}
384 1
385
		return $ignoreRequest->affected_rows();
386
	}
387 1
388
	/**
389
	 * Grabs results when the search is performed in subjects and bodies
390 1
	 *
391
	 * @param int $id_search - the id of the search
392 1
	 *
393 1
	 * @return bool|int - boolean (false) in case of errors, number of results otherwise
394
	 */
395 1
	public function getResults($id_search)
396
	{
397
		global $modSettings;
398 1
399 1
		$num_results = 0;
400
401
		$main_query = [
402 1
			'select' => [
403
				'id_search' => $id_search,
404
				'relevance' => '0',
405 1
			],
406
			'from' => '{db_prefix}topics AS t',
407
			'inner_join' => [
408
				'{db_prefix}messages AS m ON (m.id_topic = t.id_topic)'
409 1
			],
410
			'left_join' => [],
411
			'where' => [],
412 2
			'group_by' => [],
413
			'parameters' => [
414
				'min_msg' => $this->_searchParams->_minMsg,
415
				'recent_message' => $this->_searchParams->_recentMsg,
416
				'huge_topic_posts' => $this->config->humungousTopicPosts,
0 ignored issues
show
Bug Best Practice introduced by
The property humungousTopicPosts does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
417
				'short_topic_posts' => $this->config->shortTopicPosts,
0 ignored issues
show
Bug Best Practice introduced by
The property shortTopicPosts does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
418
				'is_approved' => 1,
419
				'limit' => $modSettings['search_max_results'],
420
			],
421
		];
422 2
423
		if (empty($this->_searchParams['topic']) && empty($this->_searchParams['show_complete']))
424 2
		{
425
			$main_query['select']['id_topic'] = 't.id_topic';
426 2
			$main_query['select']['id_msg'] = 'MIN(m.id_msg) AS id_msg';
427
			$main_query['select']['num_matches'] = 'COUNT(*) AS num_matches';
428
			$main_query['weights'] = $this->_weight_factors;
429 1
			$main_query['group_by'][] = 't.id_topic';
430 2
		}
431 2
		else
432
		{
433
			// This is outrageous!
434 2
			$main_query['select']['id_topic'] = 'm.id_msg AS id_topic';
435
			$main_query['select']['id_msg'] = 'm.id_msg';
436
			$main_query['select']['num_matches'] = '1 AS num_matches';
437
438
			$main_query['weights'] = [
439
				'age' => [
440
					'search' => '((m.id_msg - t.id_first_msg) / CASE WHEN t.id_last_msg = t.id_first_msg THEN 1 ELSE t.id_last_msg - t.id_first_msg END)',
441
				],
442 2
				'first_message' => [
443 2
					'search' => 'CASE WHEN m.id_msg = t.id_first_msg THEN 1 ELSE 0 END',
444 2
				],
445 2
				// experimental, give longer messages more weight
446 2
				'length' => [
447
					'search' => '(CASE WHEN LENGTH(m.body) - LENGTH(REPLACE(m.body, " ", "")) > 500 THEN 1 ELSE (LENGTH(m.body) - LENGTH(REPLACE(m.body, " ", "")) / 500) END)',
448
				],
449
			];
450 2
451
			if (!empty($this->_searchParams['topic']))
452 2
			{
453 2
				$main_query['where'][] = 't.id_topic = {int:topic}';
454 2
				$main_query['parameters']['topic'] = $this->_searchParams->topic;
455 2
			}
456 2
457
			if (!empty($this->_searchParams['show_complete']))
458
			{
459
				$main_query['group_by'][] = 'm.id_msg, t.id_first_msg, t.id_last_msg';
460
			}
461
		}
462
463
		// *** Get the subject results.
464
		$numSubjectResults = $this->_log_search_subjects($id_search);
465
466
		if ($numSubjectResults !== 0)
467
		{
468
			$main_query['weights']['subject']['search'] = 'CASE WHEN MAX(lst.id_topic) IS NULL THEN 0 ELSE 1 END';
469
			$main_query['left_join'][] = '{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics AS lst ON (' . ($this->_createTemporary ? '' : 'lst.id_search = {int:id_search} AND ') . 'lst.id_topic = t.id_topic)';
470
			if (!$this->_createTemporary)
471
			{
472
				$main_query['parameters']['id_search'] = $id_search;
473
			}
474
		}
475
476
		// Are we building an index?
477
		if ($this->useWordIndex())
478
		{
479
			$indexedResults = $this->_prepare_word_index($id_search);
480
481
			if (empty($indexedResults) && empty($numSubjectResults) && !empty($modSettings['search_force_index']))
482
			{
483
				return false;
484
			}
485
486
			if (!empty($indexedResults))
487 2
			{
488
				$main_query['inner_join'][] = '{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages AS lsm ON (lsm.id_msg = m.id_msg)';
489 2
490
				if (!$this->_createTemporary)
491 2
				{
492 2
					$main_query['where'][] = 'lsm.id_search = {int:id_search}';
493 2
					$main_query['parameters']['id_search'] = $id_search;
494
				}
495
			}
496
		}
497
		// Not using an index? All conditions have to be carried over.
498
		else
499
		{
500 2
			$orWhere = [];
501
			$count = 0;
502
			$excludedWords = $this->_searchArray->getExcludedWords();
503
			foreach ($this->_searchWords as $words)
504
			{
505
				$where = [];
506
				foreach ($words['all_words'] as $regularWord)
507
				{
508
					$where[] = 'm.body' . (in_array($regularWord, $excludedWords, true) ? ' {not_' : '{') . (empty($modSettings['search_match_words']) || $this->noRegexp() ? 'ilike} ' : 'rlike} ') . '{string:all_word_body_' . $count . '}';
509
					if (in_array($regularWord, $excludedWords, true))
510
					{
511
						$where[] = 'm.subject ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? ' {not_ilike} ' : ' {not_rlike} ') . '{string:all_word_body_' . $count . '}';
512
					}
513
514
					$main_query['parameters']['all_word_body_' . ($count++)] = $this->prepareWord($regularWord, $this->noRegexp());
515
				}
516
517
				if (!empty($where))
518
				{
519
					$orWhere[] = count($where) > 1 ? '(' . implode(' AND ', $where) . ')' : $where[0];
520
				}
521
			}
522 2
523 2
			if (!empty($orWhere))
524 2
			{
525 2
				$main_query['where'][] = count($orWhere) > 1 ? '(' . implode(' OR ', $orWhere) . ')' : $orWhere[0];
526
			}
527 2
528 2
			if (!empty($this->_searchParams->_userQuery))
529
			{
530 2
				$main_query['where'][] = '{raw:user_query}';
531 2
				$main_query['parameters']['user_query'] = $this->_searchParams->_userQuery;
532
			}
533
534
			if (!empty($this->_searchParams['topic']))
535 2
			{
536
				$main_query['where'][] = 'm.id_topic = {int:topic}';
537
				$main_query['parameters']['topic'] = $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...
538 2
			}
539
540 2
			if (!empty($this->_searchParams->_minMsgID))
541
			{
542
				$main_query['where'][] = 'm.id_msg >= {int:min_msg_id}';
543
				$main_query['parameters']['min_msg_id'] = $this->_searchParams->_minMsgID;
544 2
			}
545
546 2
			if (!empty($this->_searchParams->_maxMsgID))
547
			{
548
				$main_query['where'][] = 'm.id_msg <= {int:max_msg_id}';
549 2
				$main_query['parameters']['max_msg_id'] = $this->_searchParams->_maxMsgID;
550
			}
551
552
			if (!empty($this->_searchParams->_boardQuery))
553
			{
554
				$main_query['where'][] = 'm.id_board {raw:board_query}';
555 2
				$main_query['parameters']['board_query'] = $this->_searchParams->_boardQuery;
556
			}
557
		}
558
559
		call_integration_hook('integrate_main_search_query', [&$main_query]);
560
561 2
		// Did we either get some indexed results or otherwise did not do an indexed query?
562
		if (!empty($indexedResults) || !$this->useWordIndex())
563
		{
564
			$main_query['select']['relevance'] = $this->_build_relevance($main_query['weights']);
565
			$num_results += $this->_build_search_results_log($main_query);
566
		}
567 2
568
		// Insert subject-only matches.
569
		if ($num_results < $modSettings['search_max_results'] && $numSubjectResults !== 0)
570
		{
571
			$subject_query = [
572
				'select' => [
573 2
					'id_search' => '{int:id_search}',
574
					'id_topic' => 't.id_topic',
575
					'relevance' => $this->_build_relevance(),
576
					'id_msg' => 't.id_first_msg',
577
					'num_matches' => 1,
578
				],
579 2
				'from' => '{db_prefix}topics AS t',
580
				'inner_join' => [
581
					'{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics AS lst ON (lst.id_topic = t.id_topic)'
582 2
				],
583
				'where' => [
584 2
					$this->_createTemporary ? '1=1' : 'lst.id_search = {int:id_search}',
585 2
				],
586
				'parameters' => [
587
					'id_search' => $id_search,
588
					'min_msg' => $this->_searchParams->_minMsg,
589 2
					'recent_message' => $this->_searchParams->_recentMsg,
590
					'huge_topic_posts' => $this->config->humungousTopicPosts,
591
					'short_topic_posts' => $this->config->shortTopicPosts,
592
					'limit' => empty($modSettings['search_max_results']) ? 0 : $modSettings['search_max_results'] - $num_results,
593 2
				],
594 2
			];
595 2
596 2
			$num_results += $this->_build_search_results_log($subject_query, 'insert_log_search_results_sub_only', true);
597 2
		}
598
		elseif ($num_results === -1)
599 2
		{
600
			$num_results = 0;
601 2
		}
602
603
		return $num_results;
604 2
	}
605
606
	/**
607 2
	 * If searching in topics only (?), inserts results in log_search_topics
608 2
	 *
609 2
	 * @param int $id_search - the id of the search to delete from logs
610 2
	 *
611 2
	 * @return int - the number of search results
612
	 */
613
	private function _log_search_subjects($id_search): int
614
	{
615 2
		global $modSettings;
616
617
		if (!empty($this->_searchParams['topic']))
618
		{
619
			return 0;
620
		}
621
622 2
		$inserts = [];
623
		$numSubjectResults = 0;
624
625
		// Clean up some previous cache.
626
		if (!$this->_createTemporary)
627
		{
628
			$this->_db_search->search_query('', '
629
				DELETE FROM {db_prefix}log_search_topics
630
				WHERE id_search = {int:search_id}',
631
				[
632 2
					'search_id' => $id_search,
633
				]
634 2
			);
635
		}
636 2
637
		foreach ($this->_searchWords as $words)
638
		{
639
			$subject_query = [
640
				'from' => '{db_prefix}topics AS t',
641 2
				'inner_join' => [],
642 2
				'left_join' => [],
643
				'where' => [],
644
				'params' => [],
645 2
			];
646
647
			$numTables = 0;
648
			$prev_join = 0;
649
			$count = 0;
650
			foreach ($words['subject_words'] as $subjectWord)
651
			{
652
				$numTables++;
653
				if (in_array($subjectWord, $this->_excludedSubjectWords, true))
654
				{
655
					$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
656 2
					$subject_query['left_join'][] = '{db_prefix}log_search_subjects AS subj' . $numTables . ' ON (subj' . $numTables . '.word ' . (empty($modSettings['search_match_words']) ? '{ilike} {string:subject_not_' . $count . '}' : '= {string:subject_not_' . $count . '}') . ' AND subj' . $numTables . '.id_topic = t.id_topic)';
657
					$subject_query['params']['subject_not_' . $count] = empty($modSettings['search_match_words']) ? '%' . $subjectWord . '%' : $subjectWord;
658
659 2
					$subject_query['where'][] = '(subj' . $numTables . '.word IS NULL)';
660
					$subject_query['where'][] = 'm.body ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? ' {not_ilike} ' : ' {not_rlike} ') . '{string:body_not_' . $count . '}';
661
					$subject_query['params']['body_not_' . ($count++)] = $this->prepareWord($subjectWord, $this->noRegexp());
662
				}
663
				else
664
				{
665
					$subject_query['inner_join'][] = '{db_prefix}log_search_subjects AS subj' . $numTables . ' ON (subj' . $numTables . '.id_topic = ' . ($prev_join === 0 ? 't' : 'subj' . $prev_join) . '.id_topic)';
666 2
					$subject_query['where'][] = 'subj' . $numTables . '.word {ilike} {string:subject_like_' . $count . '}';
667 2
					$subject_query['params']['subject_like_' . ($count++)] = empty($modSettings['search_match_words']) ? '%' . $subjectWord . '%' : $subjectWord;
668 2
					$prev_join = $numTables;
669 2
				}
670
			}
671 2
672 2
			if (!empty($this->_searchParams->_userQuery))
673
			{
674
				$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
675
				$subject_query['where'][] = '{raw:user_query}';
676
				$subject_query['params']['user_query'] = $this->_searchParams->_userQuery;
677
			}
678
679
			if (!empty($this->_searchParams['topic']))
680
			{
681
				$subject_query['where'][] = 't.id_topic = {int:topic}';
682
				$subject_query['params']['topic'] = $this->_searchParams->topic;
683
			}
684 2
685 2
			if (!empty($this->_searchParams->_minMsgID))
686 2
			{
687 2
				$subject_query['where'][] = 't.id_first_msg >= {int:min_msg_id}';
688
				$subject_query['params']['min_msg_id'] = $this->_searchParams->_minMsgID;
689
			}
690
691 2
			if (!empty($this->_searchParams->_maxMsgID))
692
			{
693
				$subject_query['where'][] = 't.id_last_msg <= {int:max_msg_id}';
694
				$subject_query['params']['max_msg_id'] = $this->_searchParams->_maxMsgID;
695
			}
696
697
			if (!empty($this->_searchParams->_boardQuery))
698 2
			{
699
				$subject_query['where'][] = 't.id_board {raw:board_query}';
700
				$subject_query['params']['board_query'] = $this->_searchParams->_boardQuery;
701
			}
702
703
			if (!empty($this->_excludedPhrases))
704 2
			{
705
				$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
706
				$count = 0;
707
				foreach ($this->_excludedPhrases as $phrase)
708
				{
709
					$subject_query['where'][] = 'm.subject ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? '{not_ilike}' : '{not_rlike}') . ' {string:exclude_phrase_' . $count . '}';
710 2
					$subject_query['where'][] = 'm.body NOT ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? '{not_ilike}' : '{not_rlike}') . ' {string:exclude_phrase_' . $count . '}';
711
					$subject_query['params']['exclude_phrase_' . ($count++)] = $this->prepareWord($phrase, $this->noRegexp());
712
				}
713
			}
714
715
			call_integration_hook('integrate_subject_search_query', [&$subject_query]);
716 2
717
			// Nothing to search for?
718
			if (empty($subject_query['where']))
719
			{
720
				continue;
721
			}
722 2
723
			$ignoreRequest = $this->_db_search->search_query('', ($this->_db->support_ignore() ? ('
724
				INSERT IGNORE INTO {db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics
725
					(' . ($this->_createTemporary ? '' : 'id_search, ') . 'id_topic)') : '') . '
726
				SELECT ' . ($this->_createTemporary ? '' : $id_search . ', ') . 't.id_topic
727
				FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : '
728
					INNER JOIN ' . implode('
729
					INNER JOIN ', array_unique($subject_query['inner_join']))) . (empty($subject_query['left_join']) ? '' : '
730
					LEFT JOIN ' . implode('
731
					LEFT JOIN ', array_unique($subject_query['left_join']))) . '
732
				WHERE ' . implode('
733
					AND ', array_unique($subject_query['where'])) . (empty($modSettings['search_max_results']) ? '' : '
734 2
				LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults)),
735
				$subject_query['params']
736
			);
737 2
738
			// Don't do INSERT IGNORE? Manually fix this up!
739
			if (!$this->_db->support_ignore())
740
			{
741
				while (($row = $ignoreRequest->fetch_row()))
742 2
				{
743 1
					$ind = $this->_createTemporary ? 0 : 1;
744 2
745 2
					// No duplicates!
746 2
					if (isset($inserts[$row[$ind]]))
747 2
					{
748 2
						continue;
749
					}
750 2
751 2
					$inserts[$row[$ind]] = $row;
752 2
				}
753 2
754 2
				$ignoreRequest->free_result();
755
				$numSubjectResults = count($inserts);
756
			}
757
			else
758 2
			{
759
				$numSubjectResults += $ignoreRequest->affected_rows();
760 1
			}
761
762 1
			if (empty($modSettings['search_max_results']))
763
			{
764
				continue;
765 1
			}
766
767
			if ($numSubjectResults < $modSettings['search_max_results'])
768
			{
769
				continue;
770 1
			}
771
772 1
			break;
773 1
		}
774
775
		// Got some non-MySQL data to plonk in?
776
		if (!empty($inserts))
777 1
		{
778
			$this->_db->insert('',
779
				('{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics'),
780 2
				$this->_createTemporary ? ['id_topic' => 'int'] : ['id_search' => 'int', 'id_topic' => 'int'],
781
				$inserts,
782 1
				$this->_createTemporary ? ['id_topic'] : ['id_search', 'id_topic']
783
			);
784
		}
785
786
		return $numSubjectResults;
787 2
	}
788
789 1
	/**
790 1
	 * Determine whether to use the word index for search query
791 1
	 *
792
	 * @return bool Return true if word index should be used, otherwise false
793 1
	 */
794
	public function useWordIndex()
795
	{
796
		return false;
797 2
	}
798
799
	/**
800 2
	 * Populates log_search_messages
801
	 *
802 2
	 * @param int $id_search - the id of the search to delete from logs
803
	 *
804
	 * @return int - the number of indexed results
805
	 */
806
	private function _prepare_word_index($id_search): int
807
	{
808
		$indexedResults = 0;
809
		$inserts = [];
810
811
		// Clear, all clear!
812
		if (!$this->_createTemporary)
813
		{
814
			$this->_db_search->search_query('', '
815
				DELETE FROM {db_prefix}log_search_messages
816
				WHERE id_search = {int:id_search}',
817
				[
818
					'id_search' => $id_search,
819
				]
820
			);
821
		}
822
823
		$excludedWords = $this->_searchArray->getExcludedWords();
824
825
		foreach ($this->_searchWords as $words)
826
		{
827
			// Search for this word, assuming we have some words!
828
			if (!empty($words['indexed_words']))
829
			{
830
				// Variables required for the search.
831
				$search_data = [
832
					'insert_into' => ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages',
833
					'no_regexp' => $this->noRegexp(),
834
					'max_results' => $this->config->maxMessageResults,
0 ignored issues
show
Bug Best Practice introduced by
The property maxMessageResults does not exist on ElkArte\Helper\ValuesContainer. Since you implemented __get, consider adding a @property annotation.
Loading history...
835
					'indexed_results' => $indexedResults,
836
					'params' => [
837
						'id_search' => $this->_createTemporary ? 0 : $id_search,
838
						'excluded_words' => $excludedWords,
839
						'user_query' => empty($this->_searchParams->_userQuery) ? '' : $this->_searchParams->_userQuery,
840
						'board_query' => empty($this->_searchParams->_boardQuery) ? '' : $this->_searchParams->_boardQuery,
841
						'topic' => (int) $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...
842
						'min_msg_id' => $this->_searchParams->_minMsgID,
843
						'max_msg_id' => $this->_searchParams->_maxMsgID,
844
						'excluded_phrases' => $this->_excludedPhrases,
845
						'excluded_index_words' => $this->_excludedIndexWords,
846
						'excluded_subject_words' => $this->_excludedSubjectWords,
847
					],
848
				];
849
850
				$ignoreRequest = $this->indexedWordQuery($words, $search_data);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $ignoreRequest is correct as $this->indexedWordQuery($words, $search_data) targeting ElkArte\Search\API\Standard::indexedWordQuery() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
851
852
				if (!$this->_db->support_ignore())
853
				{
854
					while (($row = $ignoreRequest->fetch_row()))
855
					{
856
						// No duplicates!
857
						if (isset($inserts[$row[0]]))
858
						{
859
							continue;
860
						}
861
862
						$inserts[$row[0]] = $row;
863
					}
864
865
					$ignoreRequest->free_result();
866
					$indexedResults = count($inserts);
867
				}
868
				else
869
				{
870
					$indexedResults += $ignoreRequest->affected_rows();
871
				}
872
873
				if (!empty($this->config->maxMessageResults) && $indexedResults >= $this->config->maxMessageResults)
874
				{
875
					break;
876
				}
877
			}
878
		}
879
880
		// More non-MySQL stuff needed?
881
		if (!empty($inserts))
882
		{
883
			$this->_db->insert('',
884
				'{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages',
885
				$this->_createTemporary ? ['id_msg' => 'int'] : ['id_msg' => 'int', 'id_search' => 'int'],
886
				$inserts,
887
				$this->_createTemporary ? ['id_msg'] : ['id_msg', 'id_search']
888
			);
889
		}
890
891
		return $indexedResults;
892
	}
893
894
	/**
895
	 * {@inheritDoc}
896
	 */
897
	public function indexedWordQuery(array $words, array $search_data)
898
	{
899
	}
900
901
	/**
902
	 * Determines and add the relevance to the results
903
	 *
904
	 * @param array $topics - The search results (passed by reference)
905
	 * @param int $id_search - the id of the search
906
	 * @param int $start - Results are shown starting from here
907
	 * @param int $limit - No more results than this
908
	 *
909
	 * @return bool[]
910
	 */
911
	public function addRelevance(&$topics, $id_search, $start, $limit): array
912
	{
913
		// *** Retrieve the results to be shown on the page
914
		$participants = [];
915 2
		$request = $this->_db_search->search_query('', '
916
			SELECT ' .
917
			(empty($this->_searchParams['topic']) ? 'lsr.id_topic' : $this->_searchParams->topic . ' AS id_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...
918 2
				lsr.id_msg, lsr.relevance, lsr.num_matches
919 2
			FROM {db_prefix}log_search_results AS lsr' . ($this->_searchParams->sort === 'num_replies' ? '
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...
920 2
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = lsr.id_topic)' : '') . '
921 2
			WHERE lsr.id_search = {int:id_search}
922 2
			ORDER BY {raw:sort} {raw:sort_dir}
923
			LIMIT {int:limit} OFFSET {int:start}',
924
			[
925
				'id_search' => $id_search,
926
				'sort' => $this->_searchParams->sort,
927 2
				'sort_dir' => $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...
928 2
				'start' => $start,
929 2
				'limit' => $limit,
930 2
			]
931 2
		);
932
		while (($row = $request->fetch_assoc()))
933
		{
934 2
			$topics[$row['id_msg']] = [
935
				'relevance' => round($row['relevance'] / 10, 1) . '%',
936 2
				'num_matches' => $row['num_matches'],
937 2
				'matches' => [],
938 2
			];
939
			// By default, they didn't participate in the topic!
940
			$participants[$row['id_topic']] = false;
941
		}
942 2
943
		$request->free_result();
944 2
945
		return $participants;
946 2
	}
947
948
	/**
949
	 * Build out the query options based on board/topic/etc
950
	 *
951
	 * @param $query_params
952
	 * @return array
953
	 */
954
	public function queryWhereModifiers($query_params): array
955
	{
956
		$query_where = [];
957
958
		// Just by a specific user
959
		if ($query_params['user_query'])
960
		{
961
			$query_where[] = '{raw:user_query}';
962
		}
963
964
		// Just in specific boards
965
		if ($query_params['board_query'])
966
		{
967
			$query_where[] = 'm.id_board {raw:board_query}';
968
		}
969
970
		// Just search in a specific topic
971
		if ($query_params['topic'])
972
		{
973
			$query_where[] = 'm.id_topic = {int:topic}';
974
		}
975
976
		// Just in a range of messages (age)
977
		if ($query_params['min_msg_id'])
978
		{
979
			$query_where[] = 'm.id_msg >= {int:min_msg_id}';
980
		}
981
982
		if ($query_params['max_msg_id'])
983
		{
984
			$query_where[] = 'm.id_msg <= {int:max_msg_id}';
985
		}
986
987
		return $query_where;
988
	}
989
990
	/**
991
	 * Build out any subject excluded terms
992
	 *
993
	 * @param $query_params
994
	 * @param $search_data
995
	 * @return array
996
	 */
997
	public function queryExclusionModifiers(&$query_params, $search_data): array
998
	{
999
		global $modSettings;
1000
1001
		$query_where = [];
1002
1003
		$count = 0;
1004
		if (!empty($query_params['excluded_phrases']) && empty($modSettings['search_force_index']))
1005
		{
1006
			foreach ($query_params['excluded_phrases'] as $phrase)
1007
			{
1008
				$query_where[] = 'subject ' . (empty($modSettings['search_match_words']) || $search_data['no_regexp'] ? ' {not_ilike} ' : ' {not_rlike} ') . '{string:exclude_subject_phrase_' . $count . '}';
1009
				$query_params['exclude_subject_phrase_' . ($count++)] = $this->prepareWord($phrase, $search_data['no_regexp']);
1010
			}
1011
		}
1012
1013
		$count = 0;
1014
		if (!empty($query_params['excluded_subject_words']) && empty($modSettings['search_force_index']))
1015
		{
1016
			foreach ($query_params['excluded_subject_words'] as $excludedWord)
1017
			{
1018
				$query_where[] = 'subject ' . (empty($modSettings['search_match_words']) || $search_data['no_regexp'] ? ' {not_ilike} ' : ' {not_rlike} ') . '{string:exclude_subject_words_' . $count . '}';
1019
				$query_params['exclude_subject_words_' . ($count++)] = $this->prepareWord($excludedWord, $search_data['no_regexp']);
1020
			}
1021
		}
1022
1023
		return $query_where;
1024
	}
1025
}
1026