Passed
Pull Request — development (#3442)
by Elk
12:13 queued 06:23
created

Standard::searchSort()   A

Complexity

Conditions 5
Paths 16

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 3
nc 16
nop 2
dl 0
loc 6
ccs 0
cts 0
cp 0
crap 30
rs 9.6111
c 1
b 0
f 0
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\Search\Cache\Session;
20
21
/**
22
 * SearchAPI-Standard.class.php, Standard non full index, non custom index search
23
 *
24
 * @package Search
25
 */
26
class Standard extends AbstractAPI
27
{
28
	/**
29
	 * This is the last version of ElkArte that this was tested on, to protect against API changes.
30
	 *
31
	 * @var string
32
	 */
33
	public $version_compatible = 'ElkArte 2.0 dev';
34
35
	/**
36
	 * This won't work with versions of ElkArte less than this.
37
	 *
38
	 * @var string
39
	 */
40
	public $min_elk_version = 'ElkArte 1.0 Beta';
41
42
	/**
43
	 * Standard search is supported by default.
44
	 *
45
	 * @var bool
46
	 */
47
	public $is_supported = true;
48
49
	/**
50
	 *
51
	 * @var object
52
	 */
53
	protected $_search_cache = null;
54
55
	/**
56
	 *
57
	 * @var int
58
	 */
59
	protected $_num_results = 0;
60
61
	/**
62
	 * Wrapper for searchQuery of the SearchAPI
63
	 *
64
	 * @param string[] $search_words
65
	 * @param string[] $excluded_words
66
	 * @param bool[] $participants
67
	 * @param string[] $search_results
68
	 *
69
	 * @return mixed[]
70
	 * @throws \Exception
71
	 */
72 2
	public function searchQuery($search_words, $excluded_words, &$participants)
73
	{
74 2
		global $context, $modSettings;
75
76 2
		$this->_search_cache = new Session();
77 2
		$this->_searchWords = $search_words;
78 2
		$search_id = 0;
79
80 2
		if ($this->_search_cache->existsWithParams($context['params']) === false)
81
		{
82 2
			$search_id = $this->_search_cache->increaseId($modSettings['search_pointer'] ?? 0);
83
			// Store the new id right off.
84 2
			updateSettings([
85 2
				'search_pointer' => $search_id
86
			]);
87
88
			// Clear the previous cache of the final results cache.
89 2
			$this->clearCacheResults($search_id);
90
91 2
			if ($this->_searchParams['subject_only'])
92
			{
93
				$num_res = $this->getSubjectResults($search_id, $search_words, $excluded_words);
94
			}
95
			else
96
			{
97
				$num_res = $this->getResults(
98
					$search_id
99
				);
100 2
101 2
				if (empty($num_res))
102
				{
103 2
					throw new \Exception('query_not_specific_enough');
104
				}
105
			}
106
107
			$this->_search_cache->setNumResults($num_res);
108
		}
109 2
110
		// *** Retrieve the results to be shown on the page
111
		$topics = array();
112 2
		$participants = $this->addRelevance($topics, $search_id, $this->_req->getRequest('start', 'intval', 0), $modSettings['search_results_per_page']);
113
		$this->_num_results = $this->_search_cache->getNumResults();
114 2
115 2
		return $topics;
116 1
	}
117 2
118 2
	/**
119
	 * Delete logs of previous searches
120 2
	 *
121
	 * @param int $id_search - the id of the search to delete from logs
122 2
	 */
123
	public function clearCacheResults($id_search)
124
	{
125
		$this->_db_search->search_query('', '
126
			DELETE FROM {db_prefix}log_search_results
127
			WHERE id_search = {int:search_id}',
128
			array(
129
				'search_id' => $id_search,
130 2
			)
131
		);
132 2
	}
133
134
	/**
135
	 * Grabs results when the search is performed only within the subject
136 2
	 *
137
	 * @param int $id_search - the id of the search
138
	 *
139 2
	 * @return int - number of results otherwise
140
	 */
141
	protected function getSubjectResults($id_search, $search_words, $excluded_words)
142
	{
143
		global $modSettings;
144
145
		$numSubjectResults = 0;
146
147
		// We do this to try and avoid duplicate keys on databases not supporting INSERT IGNORE.
148
		foreach ($search_words as $words)
149
		{
150
			$subject_query_params = array();
151
			$subject_query = array(
152
				'from' => '{db_prefix}topics AS t',
153
				'inner_join' => array(),
154
				'left_join' => array(),
155
				'where' => array(),
156
			);
157
158
			if ($modSettings['postmod_active'])
159
			{
160
				$subject_query['where'][] = 't.approved = {int:is_approved}';
161
			}
162
163
			$numTables = 0;
164
			$prev_join = 0;
165
			$numSubjectResults = 0;
166
			foreach ($words['subject_words'] as $subjectWord)
167
			{
168
				$numTables++;
169
				if (in_array($subjectWord, $excluded_words))
170
				{
171
					$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)';
172
					$subject_query['where'][] = '(subj' . $numTables . '.word IS NULL)';
173
				}
174
				else
175
				{
176
					$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)';
177
					$subject_query['where'][] = 'subj' . $numTables . '.word ' . (empty($modSettings['search_match_words']) ? '{ilike} {string:subject_words_' . $numTables . '_wild}' : '= {string:subject_words_' . $numTables . '}');
178
					$prev_join = $numTables;
179
				}
180
181
				$subject_query_params['subject_words_' . $numTables] = $subjectWord;
182
				$subject_query_params['subject_words_' . $numTables . '_wild'] = '%' . $subjectWord . '%';
183
			}
184
185
			if (!empty($this->_searchParams->_userQuery))
186
			{
187
				$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_topic = t.id_topic)';
188
				$subject_query['where'][] = $this->_searchParams->_userQuery;
189
			}
190
191
			if (!empty($this->_searchParams['topic']))
192
			{
193
				$subject_query['where'][] = 't.id_topic = ' . $this->_searchParams['topic'];
0 ignored issues
show
Bug introduced by
Are you sure $this->_searchParams['topic'] of type array<mixed,mixed>|mixed can be used in concatenation? ( Ignorable by Annotation )

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

193
				$subject_query['where'][] = 't.id_topic = ' . /** @scrutinizer ignore-type */ $this->_searchParams['topic'];
Loading history...
194
			}
195
196
			if (!empty($this->_searchParams->_minMsgID))
197
			{
198
				$subject_query['where'][] = 't.id_first_msg >= ' . $this->_searchParams->_minMsgID;
199
			}
200
201
			if (!empty($this->_searchParams->_maxMsgID))
202
			{
203
				$subject_query['where'][] = 't.id_last_msg <= ' . $this->_searchParams->_maxMsgID;
204
			}
205
206
			if (!empty($this->_searchParams->_boardQuery))
207
			{
208
				$subject_query['where'][] = 't.id_board ' . $this->_searchParams->_boardQuery;
209
			}
210
211
			if (!empty($this->_excludedPhrases))
212
			{
213
				$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
214
215
				$count = 0;
216
				foreach ($this->_excludedPhrases as $phrase)
217
				{
218
					$subject_query['where'][] = 'm.subject ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? '{not_ilike}' : '{not_rlike}') . ' {string:excluded_phrases_' . $count . '}';
219
					$subject_query_params['excluded_phrases_' . ($count++)] = $this->prepareWord($phrase, $this->noRegexp());
220
				}
221
			}
222
223
			// Build the search query
224
			$subject_query['select'] = array(
225
				'id_search' => '{int:id_search}',
226
				'id_topic' => 't.id_topic',
227
				'relevance' => $this->_build_relevance(),
228
				'id_msg' => empty($this->_searchParams->_userQuery) ? 't.id_first_msg' : 'm.id_msg',
229
				'num_matches' => 1,
230
			);
231
232
			$subject_query['parameters'] = array_merge($subject_query_params, array(
233
				'id_search' => $id_search,
234
				'min_msg' => $this->_searchParams->_minMsg,
235
				'recent_message' => $this->_searchParams->_recentMsg,
236
				'huge_topic_posts' => $this->config->humungousTopicPosts,
237
				'is_approved' => 1,
238
				'limit' => empty($modSettings['search_max_results']) ? 0 : $modSettings['search_max_results'] - $numSubjectResults,
239
			));
240
241
			call_integration_hook('integrate_subject_only_search_query', array(&$subject_query, &$subject_query_params));
242
243
			$numSubjectResults += $this->_build_search_results_log($subject_query);
244
245
			if (!empty($modSettings['search_max_results']) && $numSubjectResults >= $modSettings['search_max_results'])
246
			{
247
				break;
248
			}
249
		}
250
251
		return $numSubjectResults;
252
	}
253
254
	/**
255
	 * If the query uses regexp or not
256
	 *
257
	 * @return bool
258
	 */
259
	protected function noRegexp()
260
	{
261
		return $this->_searchArray->getNoRegexp();
262
	}
263
264
	/**
265 2
	 * Build the search relevance query
266
	 *
267 2
	 * @param null|int[] $factors - is factors are specified that array will
268
	 * be used to build the relevance value, otherwise the function will use
269
	 * $this->_weight_factors
270
	 *
271
	 * @return string
272
	 */
273
	private function _build_relevance($factors = null)
274
	{
275
		$relevance = '1000 * (';
276
277
		if ($factors !== null && is_array($factors))
278
		{
279 2
			$weight_total = 0;
280
			foreach ($factors as $type => $value)
281 2
			{
282
				$relevance .= $this->_weight[$type];
283 2
				if (!empty($value['search']))
284
				{
285 2
					$relevance .= ' * ' . $value['search'];
286 2
				}
287
288 2
				$relevance .= ' + ';
289 2
				$weight_total += $this->_weight[$type];
290
			}
291 2
		}
292
		else
293
		{
294 2
			$weight_total = $this->_weight_total;
295 2
			foreach ($this->_weight_factors as $type => $value)
296
			{
297
				if (isset($value['results']))
298
				{
299
					$relevance .= $this->_weight[$type];
300 2
					if (!empty($value['results']))
301 2
					{
302
						$relevance .= ' * ' . $value['results'];
303 2
					}
304
305 2
					$relevance .= ' + ';
306 2
				}
307
			}
308 2
		}
309
310
		$relevance = substr($relevance, 0, -3) . ') / ' . $weight_total . ' AS relevance';
311 2
312
		return $relevance;
313
	}
314
315
	/**
316 2
	 * Inserts the data into log_search_results
317
	 *
318 2
	 * @param mixed[] $main_query - An array holding all the query parts.
319
	 *   Structure:
320
	 *        'select' => string[] - the select columns
321
	 *        'from' => string - the table for the FROM clause
322
	 *        'inner_join' => string[] - any INNER JOIN
323
	 *        'left_join' => string[] - any LEFT JOIN
324
	 *        'where' => string[] - the conditions
325
	 *        'group_by' => string[] - the fields to group by
326
	 *        'parameters' => mixed[] - any parameter required by the query
327
	 * @param string $query_identifier - a string to identify the query
328
	 * @param bool $use_old_ids - if true the topic ids retrieved by a previous
329
	 * call to this function will be used to identify duplicates
330
	 *
331
	 * @return int - the number of rows affected by the query
332
	 */
333
	private function _build_search_results_log($main_query, $query_identifier = '', $use_old_ids = false)
334
	{
335
		static $usedIDs;
336
337
		$ignoreRequest = $this->_db_search->search_query($query_identifier, ($this->_db->support_ignore() ? ('
338
			INSERT IGNORE INTO {db_prefix}log_search_results
339 2
				(' . implode(', ', array_keys($main_query['select'])) . ')') : '') . '
340
			SELECT
341 2
				' . implode(',
342
				', $main_query['select']) . '
343 2
			FROM ' . $main_query['from'] . (!empty($main_query['inner_join']) ? '
344
				INNER JOIN ' . implode('
345 2
				INNER JOIN ', array_unique($main_query['inner_join'])) : '') . (!empty($main_query['left_join']) ? '
346
				LEFT JOIN ' . implode('
347 2
				LEFT JOIN ', array_unique($main_query['left_join'])) : '') . (!empty($main_query['where']) ? '
348 2
			WHERE ' : '') . implode('
349 2
				AND ', array_unique($main_query['where'])) . (!empty($main_query['group_by']) ? '
350 2
			GROUP BY ' . implode(', ', array_unique($main_query['group_by'])) : '') . (!empty($main_query['parameters']['limit']) ? '
351 2
			LIMIT {int:limit}' : ''),
352 2
			$main_query['parameters']
353 2
		);
354 2
355 2
		// If the database doesn't support IGNORE to make this fast we need to do some tracking.
356 2
		if (!$this->_db->support_ignore())
357 2
		{
358 2
			$inserts = array();
359
360
			while (($row = $ignoreRequest->fetch_assoc()))
361
			{
362 2
				// No duplicates!
363
				if ($use_old_ids)
364 1
				{
365
					if (isset($usedIDs[$row['id_topic']]))
366 1
					{
367
						continue;
368
					}
369 1
				}
370
				elseif (isset($inserts[$row['id_topic']]))
371 1
				{
372
					continue;
373 1
				}
374
375
				$usedIDs[$row['id_topic']] = true;
376 1
				foreach ($row as $key => $value)
377
				{
378
					$inserts[$row['id_topic']][] = (int) $row[$key];
379
				}
380
			}
381 1
			$ignoreRequest->free_result();
382 1
383
			// Now put them in!
384 1
			if (!empty($inserts))
385
			{
386
				$query_columns = array();
387 1
				foreach ($main_query['select'] as $k => $v)
388
				{
389
					$query_columns[$k] = 'int';
390 1
				}
391
392 1
				$this->_db->insert('',
393 1
					'{db_prefix}log_search_results',
394
					$query_columns,
395 1
					$inserts,
396
					array('id_search', 'id_topic')
397
				);
398 1
			}
399 1
			$num_results = count($inserts);
400
		}
401
		else
402 1
		{
403
			$num_results = $ignoreRequest->affected_rows();
404
		}
405 1
406
		return $num_results;
407
	}
408
409 1
	/**
410
	 * Grabs results when the search is performed in subjects and bodies
411
	 *
412 2
	 * @param int $id_search - the id of the search
413
	 *
414
	 * @return bool|int - boolean (false) in case of errors, number of results otherwise
415
	 */
416
	public function getResults($id_search)
417
	{
418
		global $modSettings;
419
420
		$num_results = 0;
421
422 2
		$main_query = array(
423
			'select' => array(
424 2
				'id_search' => $id_search,
425
				'relevance' => '0',
426 2
			),
427
			'weights' => array(),
428
			'from' => '{db_prefix}topics AS t',
429 1
			'inner_join' => array(
430 2
				'{db_prefix}messages AS m ON (m.id_topic = t.id_topic)'
431 2
			),
432
			'left_join' => array(),
433
			'where' => array(),
434 2
			'group_by' => array(),
435
			'parameters' => array(
436
				'min_msg' => $this->_searchParams->_minMsg,
437
				'recent_message' => $this->_searchParams->_recentMsg,
438
				'huge_topic_posts' => $this->config->humungousTopicPosts,
439
				'is_approved' => 1,
440
				'limit' => $modSettings['search_max_results'],
441
			),
442 2
		);
443 2
444 2
		if (empty($this->_searchParams['topic']) && empty($this->_searchParams['show_complete']))
445 2
		{
446 2
			$main_query['select']['id_topic'] = 't.id_topic';
447
			$main_query['select']['id_msg'] = 'MAX(m.id_msg) AS id_msg';
448
			$main_query['select']['num_matches'] = 'COUNT(*) AS num_matches';
449
			$main_query['weights'] = $this->_weight_factors;
450 2
			$main_query['group_by'][] = 't.id_topic';
451
		}
452 2
		else
453 2
		{
454 2
			// This is outrageous!
455 2
			$main_query['select']['id_topic'] = 'm.id_msg AS id_topic';
456 2
			$main_query['select']['id_msg'] = 'm.id_msg';
457
			$main_query['select']['num_matches'] = '1 AS num_matches';
458
459
			$main_query['weights'] = array(
460
				'age' => array(
461
					'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)',
462
				),
463
				'first_message' => array(
464
					'search' => 'CASE WHEN m.id_msg = t.id_first_msg THEN 1 ELSE 0 END',
465
				),
466
			);
467
468
			if (!empty($this->_searchParams['topic']))
469
			{
470
				$main_query['where'][] = 't.id_topic = {int:topic}';
471
				$main_query['parameters']['topic'] = $this->_searchParams->topic;
472
			}
473
474
			if (!empty($this->_searchParams['show_complete']))
475
			{
476
				$main_query['group_by'][] = 'm.id_msg, t.id_first_msg, t.id_last_msg';
477
			}
478
		}
479
480
		// *** Get the subject results.
481
		$numSubjectResults = $this->_log_search_subjects($id_search);
482
483
		if ($numSubjectResults !== 0)
484
		{
485
			$main_query['weights']['subject']['search'] = 'CASE WHEN MAX(lst.id_topic) IS NULL THEN 0 ELSE 1 END';
486
			$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)';
487 2
			if (!$this->_createTemporary)
488
			{
489 2
				$main_query['parameters']['id_search'] = $id_search;
490
			}
491 2
		}
492 2
493 2
		// We building an index?
494
		if ($this->useWordIndex())
495
		{
496
			$indexedResults = $this->_prepare_word_index($id_search);
497
498
			if (empty($indexedResults) && empty($numSubjectResults) && !empty($modSettings['search_force_index']))
499
			{
500 2
				return false;
501
			}
502
503
			if (!empty($indexedResults))
504
			{
505
				$main_query['inner_join'][] = '{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages AS lsm ON (lsm.id_msg = m.id_msg)';
506
507
				if (!$this->_createTemporary)
508
				{
509
					$main_query['where'][] = 'lsm.id_search = {int:id_search}';
510
					$main_query['parameters']['id_search'] = $id_search;
511
				}
512
			}
513
		}
514
		// Not using an index? All conditions have to be carried over.
515
		else
516
		{
517
			$orWhere = array();
518
			$count = 0;
519
			$excludedWords = $this->_searchArray->getExcludedWords();
520
			foreach ($this->_searchWords as $words)
521
			{
522 2
				$where = array();
523 2
				foreach ($words['all_words'] as $regularWord)
524 2
				{
525 2
					$where[] = 'm.body' . (in_array($regularWord, $excludedWords) ? ' {not_' : '{') . (empty($modSettings['search_match_words']) || $this->noRegexp() ? 'ilike} ' : 'rlike} ') . '{string:all_word_body_' . $count . '}';
526
					if (in_array($regularWord, $excludedWords))
527 2
					{
528 2
						$where[] = 'm.subject ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? ' {not_ilike} ' : ' {not_rlike} ') . '{string:all_word_body_' . $count . '}';
529
					}
530 2
					$main_query['parameters']['all_word_body_' . ($count++)] = $this->prepareWord($regularWord, $this->noRegexp());
531 2
				}
532
533
				if (!empty($where))
534
				{
535 2
					$orWhere[] = count($where) > 1 ? '(' . implode(' AND ', $where) . ')' : $where[0];
536
				}
537
			}
538 2
539
			if (!empty($orWhere))
540 2
			{
541
				$main_query['where'][] = count($orWhere) > 1 ? '(' . implode(' OR ', $orWhere) . ')' : $orWhere[0];
542
			}
543
544 2
			if (!empty($this->_searchParams->_userQuery))
545
			{
546 2
				$main_query['where'][] = '{raw:user_query}';
547
				$main_query['parameters']['user_query'] = $this->_searchParams->_userQuery;
548
			}
549 2
550
			if (!empty($this->_searchParams['topic']))
551
			{
552
				$main_query['where'][] = 'm.id_topic = {int:topic}';
553
				$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...
554
			}
555 2
556
			if (!empty($this->_searchParams->_minMsgID))
557
			{
558
				$main_query['where'][] = 'm.id_msg >= {int:min_msg_id}';
559
				$main_query['parameters']['min_msg_id'] = $this->_searchParams->_minMsgID;
560
			}
561 2
562
			if (!empty($this->_searchParams->_maxMsgID))
563
			{
564
				$main_query['where'][] = 'm.id_msg <= {int:max_msg_id}';
565
				$main_query['parameters']['max_msg_id'] = $this->_searchParams->_maxMsgID;
566
			}
567 2
568
			if (!empty($this->_searchParams->_boardQuery))
569
			{
570
				$main_query['where'][] = 'm.id_board {raw:board_query}';
571
				$main_query['parameters']['board_query'] = $this->_searchParams->_boardQuery;
572
			}
573 2
		}
574
		call_integration_hook('integrate_main_search_query', array(&$main_query));
575
576
		// Did we either get some indexed results, or otherwise did not do an indexed query?
577
		if (!empty($indexedResults) || !$this->useWordIndex())
578
		{
579 2
			$main_query['select']['relevance'] = $this->_build_relevance($main_query['weights']);
580
			$num_results += $this->_build_search_results_log($main_query);
581
		}
582 2
583
		// Insert subject-only matches.
584 2
		if ($num_results < $modSettings['search_max_results'] && $numSubjectResults !== 0)
585 2
		{
586
			$subject_query = array(
587
				'select' => array(
588
					'id_search' => '{int:id_search}',
589 2
					'id_topic' => 't.id_topic',
590
					'relevance' => $this->_build_relevance(),
591
					'id_msg' => 't.id_first_msg',
592
					'num_matches' => 1,
593 2
				),
594 2
				'from' => '{db_prefix}topics AS t',
595 2
				'inner_join' => array(
596 2
					'{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics AS lst ON (lst.id_topic = t.id_topic)'
597 2
				),
598
				'where' => array(
599 2
					$this->_createTemporary ? '1=1' : 'lst.id_search = {int:id_search}',
600
				),
601 2
				'parameters' => array(
602
					'id_search' => $id_search,
603
					'min_msg' => $this->_searchParams->_minMsg,
604 2
					'recent_message' => $this->_searchParams->_recentMsg,
605
					'huge_topic_posts' => $this->config->humungousTopicPosts,
606
					'limit' => empty($modSettings['search_max_results']) ? 0 : $modSettings['search_max_results'] - $num_results,
607 2
				),
608 2
			);
609 2
610 2
			$num_results += $this->_build_search_results_log($subject_query, 'insert_log_search_results_sub_only', true);
611 2
		}
612
		elseif ($num_results === -1)
613
		{
614
			$num_results = 0;
615 2
		}
616
617
		return $num_results;
618
	}
619
620
	/**
621
	 * If searching in topics only (?), inserts results in log_search_topics
622 2
	 *
623
	 * @param int $id_search - the id of the search to delete from logs
624
	 *
625
	 * @return int - the number of search results
626
	 */
627
	private function _log_search_subjects($id_search)
628
	{
629
		global $modSettings;
630
631
		if (!empty($this->_searchParams['topic']))
632 2
		{
633
			return 0;
634 2
		}
635
636 2
		$inserts = array();
637
		$numSubjectResults = 0;
638
639
		// Clean up some previous cache.
640
		if (!$this->_createTemporary)
641 2
		{
642 2
			$this->_db_search->search_query('', '
643
				DELETE FROM {db_prefix}log_search_topics
644
				WHERE id_search = {int:search_id}',
645 2
				array(
646
					'search_id' => $id_search,
647
				)
648
			);
649
		}
650
651
		foreach ($this->_searchWords as $words)
652
		{
653
			$subject_query = array(
654
				'from' => '{db_prefix}topics AS t',
655
				'inner_join' => array(),
656 2
				'left_join' => array(),
657
				'where' => array(),
658
				'params' => array(),
659 2
			);
660
661
			$numTables = 0;
662
			$prev_join = 0;
663
			$count = 0;
664
			foreach ($words['subject_words'] as $subjectWord)
665
			{
666 2
				$numTables++;
667 2
				if (in_array($subjectWord, $this->_excludedSubjectWords))
668 2
				{
669 2
					$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
670
					$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)';
671 2
					$subject_query['params']['subject_not_' . $count] = empty($modSettings['search_match_words']) ? '%' . $subjectWord . '%' : $subjectWord;
672 2
673
					$subject_query['where'][] = '(subj' . $numTables . '.word IS NULL)';
674
					$subject_query['where'][] = 'm.body ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? ' {not_ilike} ' : ' {not_rlike} ') . '{string:body_not_' . $count . '}';
675
					$subject_query['params']['body_not_' . ($count++)] = $this->prepareWord($subjectWord, $this->noRegexp());
676
				}
677
				else
678
				{
679
					$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)';
680
					$subject_query['where'][] = 'subj' . $numTables . '.word {ilike} {string:subject_like_' . $count . '}';
681
					$subject_query['params']['subject_like_' . ($count++)] = empty($modSettings['search_match_words']) ? '%' . $subjectWord . '%' : $subjectWord;
682
					$prev_join = $numTables;
683
				}
684 2
			}
685 2
686 2
			if (!empty($this->_searchParams->_userQuery))
687 2
			{
688
				$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
689
				$subject_query['where'][] = '{raw:user_query}';
690
				$subject_query['params']['user_query'] = $this->_searchParams->_userQuery;
691 2
			}
692
693
			if (!empty($this->_searchParams['topic']))
694
			{
695
				$subject_query['where'][] = 't.id_topic = {int:topic}';
696
				$subject_query['params']['topic'] = $this->_searchParams->topic;
697
			}
698 2
699
			if (!empty($this->_searchParams->_minMsgID))
700
			{
701
				$subject_query['where'][] = 't.id_first_msg >= {int:min_msg_id}';
702
				$subject_query['params']['min_msg_id'] = $this->_searchParams->_minMsgID;
703
			}
704 2
705
			if (!empty($this->_searchParams->_maxMsgID))
706
			{
707
				$subject_query['where'][] = 't.id_last_msg <= {int:max_msg_id}';
708
				$subject_query['params']['max_msg_id'] = $this->_searchParams->_maxMsgID;
709
			}
710 2
711
			if (!empty($this->_searchParams->_boardQuery))
712
			{
713
				$subject_query['where'][] = 't.id_board {raw:board_query}';
714
				$subject_query['params']['board_query'] = $this->_searchParams->_boardQuery;
715
			}
716 2
717
			if (!empty($this->_excludedPhrases))
718
			{
719
				$subject_query['inner_join'][] = '{db_prefix}messages AS m ON (m.id_msg = t.id_first_msg)';
720
				$count = 0;
721
				foreach ($this->_excludedPhrases as $phrase)
722 2
				{
723
					$subject_query['where'][] = 'm.subject ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? '{not_ilike}' : '{not_rlike}') . ' {string:exclude_phrase_' . $count . '}';
724
					$subject_query['where'][] = 'm.body NOT ' . (empty($modSettings['search_match_words']) || $this->noRegexp() ? '{not_ilike}' : '{not_rlike}') . ' {string:exclude_phrase_' . $count . '}';
725
					$subject_query['params']['exclude_phrase_' . ($count++)] = $this->prepareWord($phrase, $this->noRegexp());
726
				}
727
			}
728
729
			call_integration_hook('integrate_subject_search_query', array(&$subject_query));
730
731
			// Nothing to search for?
732
			if (empty($subject_query['where']))
733
			{
734 2
				continue;
735
			}
736
737 2
			$ignoreRequest = $this->_db_search->search_query('', ($this->_db->support_ignore() ? ('
738
				INSERT IGNORE INTO {db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics
739
					(' . ($this->_createTemporary ? '' : 'id_search, ') . 'id_topic)') : '') . '
740
				SELECT ' . ($this->_createTemporary ? '' : $id_search . ', ') . 't.id_topic
741
				FROM ' . $subject_query['from'] . (empty($subject_query['inner_join']) ? '' : '
742 2
					INNER JOIN ' . implode('
743 1
					INNER JOIN ', array_unique($subject_query['inner_join']))) . (empty($subject_query['left_join']) ? '' : '
744 2
					LEFT JOIN ' . implode('
745 2
					LEFT JOIN ', array_unique($subject_query['left_join']))) . '
746 2
				WHERE ' . implode('
747 2
					AND ', array_unique($subject_query['where'])) . (empty($modSettings['search_max_results']) ? '' : '
748 2
				LIMIT ' . ($modSettings['search_max_results'] - $numSubjectResults)),
749
				$subject_query['params']
750 2
			);
751 2
752 2
			// Don't do INSERT IGNORE? Manually fix this up!
753 2
			if (!$this->_db->support_ignore())
754 2
			{
755
				while (($row = $ignoreRequest->fetch_row()))
756
				{
757
					$ind = $this->_createTemporary ? 0 : 1;
758 2
759
					// No duplicates!
760 1
					if (isset($inserts[$row[$ind]]))
761
					{
762 1
						continue;
763
					}
764
765 1
					$inserts[$row[$ind]] = $row;
766
				}
767
				$ignoreRequest->free_result();
768
				$numSubjectResults = count($inserts);
769
			}
770 1
			else
771
			{
772 1
				$numSubjectResults += $ignoreRequest->affected_rows();
773 1
			}
774
775
			if (!empty($modSettings['search_max_results']) && $numSubjectResults >= $modSettings['search_max_results'])
776
			{
777 1
				break;
778
			}
779
		}
780 2
781
		// Got some non-MySQL data to plonk in?
782 1
		if (!empty($inserts))
783
		{
784
			$this->_db->insert('',
785
				('{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_topics'),
786
				$this->_createTemporary ? array('id_topic' => 'int') : array('id_search' => 'int', 'id_topic' => 'int'),
787 2
				$inserts,
788
				$this->_createTemporary ? array('id_topic') : array('id_search', 'id_topic')
789 1
			);
790 1
		}
791 1
792
		return $numSubjectResults;
793 1
	}
794
795
	public function useWordIndex()
796
	{
797 2
		return false;
798
	}
799
800 2
	/**
801
	 * Populates log_search_messages
802 2
	 *
803
	 * @param int $id_search - the id of the search to delete from logs
804
	 *
805
	 * @return int - the number of indexed results
806
	 */
807
	private function _prepare_word_index($id_search)
808
	{
809
		$indexedResults = 0;
810
		$inserts = array();
811
812
		// Clear, all clear!
813
		if (!$this->_createTemporary)
814
		{
815
			$this->_db_search->search_query('', '
816
				DELETE FROM {db_prefix}log_search_messages
817
				WHERE id_search = {int:id_search}',
818
				array(
819
					'id_search' => $id_search,
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 = array(
832
					'insert_into' => ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages',
833
					'no_regexp' => $this->noRegexp(),
834
					'max_results' => $this->config->maxMessageResults,
835
					'indexed_results' => $indexedResults,
836
					'params' => array(
837
						'id_search' => !$this->_createTemporary ? $id_search : 0,
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' => (int) $this->_searchParams->_minMsgID,
843
						'max_msg_id' => (int) $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
					$ignoreRequest->free_result();
865
					$indexedResults = count($inserts);
866
				}
867
				else
868
				{
869
					$indexedResults += $ignoreRequest->affected_rows();
870
				}
871
872
				if (!empty($this->config->maxMessageResults) && $indexedResults >= $this->config->maxMessageResults)
873
				{
874
					break;
875
				}
876
			}
877
		}
878
879
		// More non-MySQL stuff needed?
880
		if (!empty($inserts))
881
		{
882
			$this->_db->insert('',
883
				'{db_prefix}' . ($this->_createTemporary ? 'tmp_' : '') . 'log_search_messages',
884
				$this->_createTemporary ? array('id_msg' => 'int') : array('id_msg' => 'int', 'id_search' => 'int'),
885
				$inserts,
886
				$this->_createTemporary ? array('id_msg') : array('id_msg', 'id_search')
887
			);
888
		}
889
890
		return $indexedResults;
891
	}
892
893
	/**
894
	 * {@inheritdoc }
895
	 */
896
	public function indexedWordQuery($words, $search_data)
897
	{
898
	}
899
900
	/**
901
	 * Determines and add the relevance to the results
902
	 *
903
	 * @param mixed[] $topics - The search results (passed by reference)
904
	 * @param int $id_search - the id of the search
905
	 * @param int $start - Results are shown starting from here
906
	 * @param int $limit - No more results than this
907
	 *
908
	 * @return bool[]
909
	 */
910
	public function addRelevance(&$topics, $id_search, $start, $limit)
911
	{
912
		// *** Retrieve the results to be shown on the page
913
		$participants = array();
914
		$request = $this->_db_search->search_query('', '
915 2
			SELECT ' . (empty($this->_searchParams['topic']) ? 'lsr.id_topic' : $this->_searchParams->topic . ' AS id_topic') . ', lsr.id_msg, lsr.relevance, lsr.num_matches
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...
916
			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...
917
				INNER JOIN {db_prefix}topics AS t ON (t.id_topic = lsr.id_topic)' : '') . '
918 2
			WHERE lsr.id_search = {int:id_search}
919 2
			ORDER BY {raw:sort} {raw:sort_dir}
920 2
			LIMIT {int:limit} OFFSET {int:start}',
921 2
			array(
922 2
				'id_search' => $id_search,
923
				'sort' => $this->_searchParams->sort,
924
				'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...
925
				'start' => $start,
926
				'limit' => $limit,
927 2
			)
928 2
		);
929 2
		while (($row = $request->fetch_assoc()))
930 2
		{
931 2
			$topics[$row['id_msg']] = array(
932
				'relevance' => round($row['relevance'] / 10, 1) . '%',
933
				'num_matches' => $row['num_matches'],
934 2
				'matches' => array(),
935
			);
936 2
			// By default they didn't participate in the topic!
937 2
			$participants[$row['id_topic']] = false;
938 2
		}
939
		$request->free_result();
940
941
		return $participants;
942 2
	}
943
944 2
	/**
945
	 * Build out the query options based on board/topic/etc
946 2
	 *
947
	 * @param $query_params
948
	 * @return array
949
	 */
950
	public function queryWhereModifiers($query_params)
951
	{
952
		$query_where = [];
953
954
		// Just by a specific user
955
		if ($query_params['user_query'])
956
		{
957
			$query_where[] = '{raw:user_query}';
958
		}
959
960
		// Just in specific boards
961
		if ($query_params['board_query'])
962
		{
963
			$query_where[] = 'm.id_board {raw:board_query}';
964
		}
965
966
		// Just search in a specific topic
967
		if ($query_params['topic'])
968
		{
969
			$query_where[] = 'm.id_topic = {int:topic}';
970
		}
971
972
		// Just in a range of messages (age)
973
		if ($query_params['min_msg_id'])
974
		{
975
			$query_where[] = 'm.id_msg >= {int:min_msg_id}';
976
		}
977
978
		if ($query_params['max_msg_id'])
979
		{
980
			$query_where[] = 'm.id_msg <= {int:max_msg_id}';
981
		}
982
983
		return $query_where;
984
	}
985
986
	/**
987
	 * Build out any subject excluded terms
988
	 *
989
	 * @param $query_params
990
	 * @param $search_data
991
	 * @return array
992
	 */
993
	public function queryExclusionModifiers(&$query_params, $search_data)
994
	{
995
		global $modSettings;
996
997
		$query_where = [];
998
999
		$count = 0;
1000
		if (!empty($query_params['excluded_phrases']) && empty($modSettings['search_force_index']))
1001
		{
1002
			foreach ($query_params['excluded_phrases'] as $phrase)
1003
			{
1004
				$query_where[] = 'subject ' . (empty($modSettings['search_match_words']) || $search_data['no_regexp'] ? ' {not_ilike} ' : ' {not_rlike} ') . '{string:exclude_subject_phrase_' . $count . '}';
1005
				$query_params['exclude_subject_phrase_' . ($count++)] = $this->prepareWord($phrase, $search_data['no_regexp']);
1006
			}
1007
		}
1008
1009
		$count = 0;
1010
		if (!empty($query_params['excluded_subject_words']) && empty($modSettings['search_force_index']))
1011
		{
1012
			foreach ($query_params['excluded_subject_words'] as $excludedWord)
1013
			{
1014
				$query_where[] = 'subject ' . (empty($modSettings['search_match_words']) || $search_data['no_regexp'] ? ' {not_ilike} ' : ' {not_rlike} ') . '{string:exclude_subject_words_' . $count . '}';
1015
				$query_params['exclude_subject_words_' . ($count++)] = $this->prepareWord($excludedWord, $search_data['no_regexp']);
1016
			}
1017
		}
1018
1019
		return $query_where;
1020
	}
1021
}
1022