Standard::_log_search_subjects()   F
last analyzed

Complexity

Conditions 41
Paths > 20000

Size

Total Lines 174
Code Lines 93

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 51
CRAP Score 131.791

Importance

Changes 0
Metric Value
cc 41
eloc 93
dl 0
loc 174
rs 0
c 0
b 0
f 0
nc 37637
nop 1
ccs 51
cts 82
cp 0.622
crap 131.791

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