PageRenderTime 57ms CodeModel.GetById 25ms RepoModel.GetById 1ms app.codeStats 0ms

/components/com_finder/models/search.php

https://bitbucket.org/biojazzard/joomla-eboracast
PHP | 1235 lines | 617 code | 179 blank | 439 comment | 80 complexity | 6ab38b5083534ba276dcca0e85053ec4 MD5 | raw file
Possible License(s): LGPL-2.1, GPL-2.0, MIT, BSD-3-Clause
  1. <?php
  2. /**
  3. * @package Joomla.Site
  4. * @subpackage com_finder
  5. *
  6. * @copyright Copyright (C) 2005 - 2013 Open Source Matters, Inc. All rights reserved.
  7. * @license GNU General Public License version 2 or later; see LICENSE
  8. */
  9. defined('_JEXEC') or die;
  10. // Register dependent classes.
  11. define('FINDER_PATH_INDEXER', JPATH_ADMINISTRATOR . '/components/com_finder/helpers/indexer');
  12. JLoader::register('FinderIndexerHelper', FINDER_PATH_INDEXER . '/helper.php');
  13. JLoader::register('FinderIndexerQuery', FINDER_PATH_INDEXER . '/query.php');
  14. JLoader::register('FinderIndexerResult', FINDER_PATH_INDEXER . '/result.php');
  15. JLoader::register('FinderIndexerStemmer', FINDER_PATH_INDEXER . '/stemmer.php');
  16. /**
  17. * Search model class for the Finder package.
  18. *
  19. * @package Joomla.Site
  20. * @subpackage com_finder
  21. * @since 2.5
  22. */
  23. class FinderModelSearch extends JModelList
  24. {
  25. /**
  26. * Context string for the model type
  27. *
  28. * @var string
  29. * @since 2.5
  30. */
  31. protected $context = 'com_finder.search';
  32. /**
  33. * The query object is an instance of FinderIndexerQuery which contains and
  34. * models the entire search query including the text input; static and
  35. * dynamic taxonomy filters; date filters; etc.
  36. *
  37. * @var FinderIndexerQuery
  38. * @since 2.5
  39. */
  40. protected $query;
  41. /**
  42. * An array of all excluded terms ids.
  43. *
  44. * @var array
  45. * @since 2.5
  46. */
  47. protected $excludedTerms = array();
  48. /**
  49. * An array of all included terms ids.
  50. *
  51. * @var array
  52. * @since 2.5
  53. */
  54. protected $includedTerms = array();
  55. /**
  56. * An array of all required terms ids.
  57. *
  58. * @var array
  59. * @since 2.5
  60. */
  61. protected $requiredTerms = array();
  62. /**
  63. * Method to get the results of the query.
  64. *
  65. * @return array An array of FinderIndexerResult objects.
  66. *
  67. * @since 2.5
  68. * @throws Exception on database error.
  69. */
  70. public function getResults()
  71. {
  72. // Check if the search query is valid.
  73. if (empty($this->query->search))
  74. {
  75. return null;
  76. }
  77. // Check if we should return results.
  78. if (empty($this->includedTerms) && (empty($this->query->filters) || !$this->query->empty))
  79. {
  80. return null;
  81. }
  82. // Get the store id.
  83. $store = $this->getStoreId('getResults');
  84. // Use the cached data if possible.
  85. if ($this->retrieve($store))
  86. {
  87. return $this->retrieve($store);
  88. }
  89. // Get the row data.
  90. $items = $this->getResultsData();
  91. // Check the data.
  92. if (empty($items))
  93. {
  94. return null;
  95. }
  96. // Create the query to get the search results.
  97. $db = $this->getDbo();
  98. $query = $db->getQuery(true)
  99. ->select($db->quoteName('link_id') . ', ' . $db->quoteName('object'))
  100. ->from($db->quoteName('#__finder_links'))
  101. ->where($db->quoteName('link_id') . ' IN (' . implode(',', array_keys($items)) . ')');
  102. // Load the results from the database.
  103. $db->setQuery($query);
  104. $rows = $db->loadObjectList('link_id');
  105. // Set up our results container.
  106. $results = $items;
  107. // Convert the rows to result objects.
  108. foreach ($rows as $rk => $row)
  109. {
  110. // Build the result object.
  111. $result = unserialize($row->object);
  112. $result->weight = $results[$rk];
  113. $result->link_id = $rk;
  114. // Add the result back to the stack.
  115. $results[$rk] = $result;
  116. }
  117. // Switch to a non-associative array.
  118. $results = array_values($results);
  119. // Push the results into cache.
  120. $this->store($store, $results);
  121. // Return the results.
  122. return $this->retrieve($store);
  123. }
  124. /**
  125. * Method to get the total number of results.
  126. *
  127. * @return integer The total number of results.
  128. *
  129. * @since 2.5
  130. * @throws Exception on database error.
  131. */
  132. public function getTotal()
  133. {
  134. // Check if the search query is valid.
  135. if (empty($this->query->search))
  136. {
  137. return null;
  138. }
  139. // Check if we should return results.
  140. if (empty($this->includedTerms) && (empty($this->query->filters) || !$this->query->empty))
  141. {
  142. return null;
  143. }
  144. // Get the store id.
  145. $store = $this->getStoreId('getTotal');
  146. // Use the cached data if possible.
  147. if ($this->retrieve($store))
  148. {
  149. return $this->retrieve($store);
  150. }
  151. // Get the results total.
  152. $total = $this->getResultsTotal();
  153. // Push the total into cache.
  154. $this->store($store, $total);
  155. // Return the total.
  156. return $this->retrieve($store);
  157. }
  158. /**
  159. * Method to get the query object.
  160. *
  161. * @return FinderIndexerQuery A query object.
  162. *
  163. * @since 2.5
  164. */
  165. public function getQuery()
  166. {
  167. // Return the query object.
  168. return $this->query;
  169. }
  170. /**
  171. * Method to build a database query to load the list data.
  172. *
  173. * @return JDatabaseQuery A database query.
  174. *
  175. * @since 2.5
  176. */
  177. protected function getListQuery()
  178. {
  179. // Get the store id.
  180. $store = $this->getStoreId('getListQuery');
  181. // Use the cached data if possible.
  182. if ($this->retrieve($store, false))
  183. {
  184. return clone($this->retrieve($store, false));
  185. }
  186. // Set variables
  187. $user = JFactory::getUser();
  188. $groups = implode(',', $user->getAuthorisedViewLevels());
  189. // Create a new query object.
  190. $db = $this->getDbo();
  191. $query = $db->getQuery(true)
  192. ->select('l.link_id')
  193. ->from($db->quoteName('#__finder_links') . ' AS l')
  194. ->where('l.access IN (' . $groups . ')')
  195. ->where('l.state = 1');
  196. // Get the null date and the current date, minus seconds.
  197. $nullDate = $db->quote($db->getNullDate());
  198. $nowDate = $db->quote(substr_replace(JFactory::getDate()->toSQL(), '00', -2));
  199. // Add the publish up and publish down filters.
  200. $query->where('(l.publish_start_date = ' . $nullDate . ' OR l.publish_end_date <= ' . $nowDate . ')')
  201. ->where('(l.publish_end_date = ' . $nullDate . ' OR l.publish_end_date >= ' . $nowDate . ')');
  202. /*
  203. * Add the taxonomy filters to the query. We have to join the taxonomy
  204. * map table for each group so that we can use AND clauses across
  205. * groups. Within each group there can be an array of values that will
  206. * use OR clauses.
  207. */
  208. if (!empty($this->query->filters))
  209. {
  210. // Convert the associative array to a numerically indexed array.
  211. $groups = array_values($this->query->filters);
  212. // Iterate through each taxonomy group and add the join and where.
  213. for ($i = 0, $c = count($groups); $i < $c; $i++)
  214. {
  215. // We use the offset because each join needs a unique alias.
  216. $query->join('INNER', $db->quoteName('#__finder_taxonomy_map') . ' AS t' . $i . ' ON t' . $i . '.link_id = l.link_id')
  217. ->where('t' . $i . '.node_id IN (' . implode(',', $groups[$i]) . ')');
  218. }
  219. }
  220. // Add the start date filter to the query.
  221. if (!empty($this->query->date1))
  222. {
  223. // Escape the date.
  224. $date1 = $db->quote($this->query->date1);
  225. // Add the appropriate WHERE condition.
  226. if ($this->query->when1 == 'before')
  227. {
  228. $query->where($db->quoteName('l.start_date') . ' <= ' . $date1);
  229. }
  230. elseif ($this->query->when1 == 'after')
  231. {
  232. $query->where($db->quoteName('l.start_date') . ' >= ' . $date1);
  233. }
  234. else
  235. {
  236. $query->where($db->quoteName('l.start_date') . ' = ' . $date1);
  237. }
  238. }
  239. // Add the end date filter to the query.
  240. if (!empty($this->query->date2))
  241. {
  242. // Escape the date.
  243. $date2 = $db->quote($this->query->date2);
  244. // Add the appropriate WHERE condition.
  245. if ($this->query->when2 == 'before')
  246. {
  247. $query->where($db->quoteName('l.start_date') . ' <= ' . $date2);
  248. }
  249. elseif ($this->query->when2 == 'after')
  250. {
  251. $query->where($db->quoteName('l.start_date') . ' >= ' . $date2);
  252. }
  253. else
  254. {
  255. $query->where($db->quoteName('l.start_date') . ' = ' . $date2);
  256. }
  257. }
  258. // Filter by language
  259. if ($this->getState('filter.language'))
  260. {
  261. $query->where('l.language IN (' . $db->quote(JFactory::getLanguage()->getTag()) . ', ' . $db->quote('*') . ')');
  262. }
  263. // Push the data into cache.
  264. $this->store($store, $query, false);
  265. // Return a copy of the query object.
  266. return clone($this->retrieve($store, false));
  267. }
  268. /**
  269. * Method to get the total number of results for the search query.
  270. *
  271. * @return integer The results total.
  272. *
  273. * @since 2.5
  274. * @throws Exception on database error.
  275. */
  276. protected function getResultsTotal()
  277. {
  278. // Get the store id.
  279. $store = $this->getStoreId('getResultsTotal', false);
  280. // Use the cached data if possible.
  281. if ($this->retrieve($store))
  282. {
  283. return $this->retrieve($store);
  284. }
  285. // Get the base query and add the ordering information.
  286. $base = $this->getListQuery();
  287. $base->select('0 AS ordering');
  288. // Get the maximum number of results.
  289. $limit = (int) $this->getState('match.limit');
  290. /*
  291. * If there are no optional or required search terms in the query,
  292. * we can get the result total in one relatively simple database query.
  293. */
  294. if (empty($this->includedTerms))
  295. {
  296. // Adjust the query to join on the appropriate mapping table.
  297. $query = clone($base);
  298. $query->clear('select')
  299. ->select('COUNT(DISTINCT l.link_id)');
  300. // Get the total from the database.
  301. $this->_db->setQuery($query);
  302. $total = $this->_db->loadResult();
  303. // Push the total into cache.
  304. $this->store($store, min($total, $limit));
  305. // Return the total.
  306. return $this->retrieve($store);
  307. }
  308. /*
  309. * If there are optional or required search terms in the query, the
  310. * process of getting the result total is more complicated.
  311. */
  312. $start = 0;
  313. $items = array();
  314. $sorted = array();
  315. $maps = array();
  316. $excluded = $this->getExcludedLinkIds();
  317. /*
  318. * Iterate through the included search terms and group them by mapping
  319. * table suffix. This ensures that we never have to do more than 16
  320. * queries to get a batch. This may seem like a lot but it is rarely
  321. * anywhere near 16 because of the improved mapping algorithm.
  322. */
  323. foreach ($this->includedTerms as $token => $ids)
  324. {
  325. // Get the mapping table suffix.
  326. $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
  327. // Initialize the mapping group.
  328. if (!array_key_exists($suffix, $maps))
  329. {
  330. $maps[$suffix] = array();
  331. }
  332. // Add the terms to the mapping group.
  333. $maps[$suffix] = array_merge($maps[$suffix], $ids);
  334. }
  335. /*
  336. * When the query contains search terms we need to find and process the
  337. * result total iteratively using a do-while loop.
  338. */
  339. do
  340. {
  341. // Create a container for the fetched results.
  342. $results = array();
  343. $more = false;
  344. /*
  345. * Iterate through the mapping groups and load the total from each
  346. * mapping table.
  347. */
  348. foreach ($maps as $suffix => $ids)
  349. {
  350. // Create a storage key for this set.
  351. $setId = $this->getStoreId('getResultsTotal:' . serialize(array_values($ids)) . ':' . $start . ':' . $limit);
  352. // Use the cached data if possible.
  353. if ($this->retrieve($setId))
  354. {
  355. $temp = $this->retrieve($setId);
  356. }
  357. // Load the data from the database.
  358. else
  359. {
  360. // Adjust the query to join on the appropriate mapping table.
  361. $query = clone($base);
  362. $query->join('INNER', '#__finder_links_terms' . $suffix . ' AS m ON m.link_id = l.link_id')
  363. ->where('m.term_id IN (' . implode(',', $ids) . ')');
  364. // Load the results from the database.
  365. $this->_db->setQuery($query, $start, $limit);
  366. $temp = $this->_db->loadObjectList();
  367. // Set the more flag to true if any of the sets equal the limit.
  368. $more = (count($temp) === $limit) ? true : false;
  369. // We loaded the data unkeyed but we need it to be keyed for later.
  370. $junk = $temp;
  371. $temp = array();
  372. // Convert to an associative array.
  373. for ($i = 0, $c = count($junk); $i < $c; $i++)
  374. {
  375. $temp[$junk[$i]->link_id] = $junk[$i];
  376. }
  377. // Store this set in cache.
  378. $this->store($setId, $temp);
  379. }
  380. // Merge the results.
  381. $results = array_merge($results, $temp);
  382. }
  383. // Check if there are any excluded terms to deal with.
  384. if (count($excluded))
  385. {
  386. // Remove any results that match excluded terms.
  387. for ($i = 0, $c = count($results); $i < $c; $i++)
  388. {
  389. if (in_array($results[$i]->link_id, $excluded))
  390. {
  391. unset($results[$i]);
  392. }
  393. }
  394. // Reset the array keys.
  395. $results = array_values($results);
  396. }
  397. // Iterate through the set to extract the unique items.
  398. for ($i = 0, $c = count($results); $i < $c; $i++)
  399. {
  400. if (!isset($sorted[$results[$i]->link_id]))
  401. {
  402. $sorted[$results[$i]->link_id] = $results[$i]->ordering;
  403. }
  404. }
  405. /*
  406. * If the query contains just optional search terms and we have
  407. * enough items for the page, we can stop here.
  408. */
  409. if (empty($this->requiredTerms))
  410. {
  411. // If we need more items and they're available, make another pass.
  412. if ($more && count($sorted) < $limit)
  413. {
  414. // Increment the batch starting point and continue.
  415. $start += $limit;
  416. continue;
  417. }
  418. // Push the total into cache.
  419. $this->store($store, min(count($sorted), $limit));
  420. // Return the total.
  421. return $this->retrieve($store);
  422. }
  423. /*
  424. * The query contains required search terms so we have to iterate
  425. * over the items and remove any items that do not match all of the
  426. * required search terms. This is one of the most expensive steps
  427. * because a required token could theoretically eliminate all of
  428. * current terms which means we would have to loop through all of
  429. * the possibilities.
  430. */
  431. foreach ($this->requiredTerms as $token => $required)
  432. {
  433. // Create a storage key for this set.
  434. $setId = $this->getStoreId('getResultsTotal:required:' . serialize(array_values($required)) . ':' . $start . ':' . $limit);
  435. // Use the cached data if possible.
  436. if ($this->retrieve($setId))
  437. {
  438. $reqTemp = $this->retrieve($setId);
  439. }
  440. // Check if the token was matched.
  441. elseif (empty($required))
  442. {
  443. return null;
  444. }
  445. // Load the data from the database.
  446. else
  447. {
  448. // Setup containers in case we have to make multiple passes.
  449. $reqStart = 0;
  450. $reqTemp = array();
  451. do
  452. {
  453. // Get the map table suffix.
  454. $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
  455. // Adjust the query to join on the appropriate mapping table.
  456. $query = clone($base);
  457. $query->join('INNER', '#__finder_links_terms' . $suffix . ' AS m ON m.link_id = l.link_id')
  458. ->where('m.term_id IN (' . implode(',', $required) . ')');
  459. // Load the results from the database.
  460. $this->_db->setQuery($query, $reqStart, $limit);
  461. $temp = $this->_db->loadObjectList('link_id');
  462. // Set the required token more flag to true if the set equal the limit.
  463. $reqMore = (count($temp) === $limit) ? true : false;
  464. // Merge the matching set for this token.
  465. $reqTemp = $reqTemp + $temp;
  466. // Increment the term offset.
  467. $reqStart += $limit;
  468. }
  469. while ($reqMore == true);
  470. // Store this set in cache.
  471. $this->store($setId, $reqTemp);
  472. }
  473. // Remove any items that do not match the required term.
  474. $sorted = array_intersect_key($sorted, $reqTemp);
  475. }
  476. // If we need more items and they're available, make another pass.
  477. if ($more && count($sorted) < $limit)
  478. {
  479. // Increment the batch starting point.
  480. $start += $limit;
  481. // Merge the found items.
  482. $items = $items + $sorted;
  483. continue;
  484. }
  485. // Otherwise, end the loop.
  486. {
  487. // Merge the found items.
  488. $items = $items + $sorted;
  489. $more = false;
  490. }
  491. // End do-while loop.
  492. }
  493. while ($more === true);
  494. // Set the total.
  495. $total = count($items);
  496. $total = min($total, $limit);
  497. // Push the total into cache.
  498. $this->store($store, $total);
  499. // Return the total.
  500. return $this->retrieve($store);
  501. }
  502. /**
  503. * Method to get the results for the search query.
  504. *
  505. * @return array An array of result data objects.
  506. *
  507. * @since 2.5
  508. * @throws Exception on database error.
  509. */
  510. protected function getResultsData()
  511. {
  512. // Get the store id.
  513. $store = $this->getStoreId('getResultsData', false);
  514. // Use the cached data if possible.
  515. if ($this->retrieve($store))
  516. {
  517. return $this->retrieve($store);
  518. }
  519. // Get the result ordering and direction.
  520. $ordering = $this->getState('list.ordering', 'l.start_date');
  521. $direction = $this->getState('list.direction', 'DESC');
  522. // Get the base query and add the ordering information.
  523. $base = $this->getListQuery();
  524. $base->select($this->_db->escape($ordering) . ' AS ordering');
  525. $base->order($this->_db->escape($ordering) . ' ' . $this->_db->escape($direction));
  526. /*
  527. * If there are no optional or required search terms in the query, we
  528. * can get the results in one relatively simple database query.
  529. */
  530. if (empty($this->includedTerms))
  531. {
  532. // Get the results from the database.
  533. $this->_db->setQuery($base, (int) $this->getState('list.start'), (int) $this->getState('list.limit'));
  534. $return = $this->_db->loadObjectList('link_id');
  535. // Get a new store id because this data is page specific.
  536. $store = $this->getStoreId('getResultsData', true);
  537. // Push the results into cache.
  538. $this->store($store, $return);
  539. // Return the results.
  540. return $this->retrieve($store);
  541. }
  542. /*
  543. * If there are optional or required search terms in the query, the
  544. * process of getting the results is more complicated.
  545. */
  546. $start = 0;
  547. $limit = (int) $this->getState('match.limit');
  548. $items = array();
  549. $sorted = array();
  550. $maps = array();
  551. $excluded = $this->getExcludedLinkIds();
  552. /*
  553. * Iterate through the included search terms and group them by mapping
  554. * table suffix. This ensures that we never have to do more than 16
  555. * queries to get a batch. This may seem like a lot but it is rarely
  556. * anywhere near 16 because of the improved mapping algorithm.
  557. */
  558. foreach ($this->includedTerms as $token => $ids)
  559. {
  560. // Get the mapping table suffix.
  561. $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
  562. // Initialize the mapping group.
  563. if (!array_key_exists($suffix, $maps))
  564. {
  565. $maps[$suffix] = array();
  566. }
  567. // Add the terms to the mapping group.
  568. $maps[$suffix] = array_merge($maps[$suffix], $ids);
  569. }
  570. /*
  571. * When the query contains search terms we need to find and process the
  572. * results iteratively using a do-while loop.
  573. */
  574. do
  575. {
  576. // Create a container for the fetched results.
  577. $results = array();
  578. $more = false;
  579. /*
  580. * Iterate through the mapping groups and load the results from each
  581. * mapping table.
  582. */
  583. foreach ($maps as $suffix => $ids)
  584. {
  585. // Create a storage key for this set.
  586. $setId = $this->getStoreId('getResultsData:' . serialize(array_values($ids)) . ':' . $start . ':' . $limit);
  587. // Use the cached data if possible.
  588. if ($this->retrieve($setId))
  589. {
  590. $temp = $this->retrieve($setId);
  591. }
  592. // Load the data from the database.
  593. else
  594. {
  595. // Adjust the query to join on the appropriate mapping table.
  596. $query = clone($base);
  597. $query->join('INNER', $this->_db->quoteName('#__finder_links_terms' . $suffix) . ' AS m ON m.link_id = l.link_id')
  598. ->where('m.term_id IN (' . implode(',', $ids) . ')');
  599. // Load the results from the database.
  600. $this->_db->setQuery($query, $start, $limit);
  601. $temp = $this->_db->loadObjectList('link_id');
  602. // Store this set in cache.
  603. $this->store($setId, $temp);
  604. // The data is keyed by link_id to ease caching, we don't need it till later.
  605. $temp = array_values($temp);
  606. }
  607. // Set the more flag to true if any of the sets equal the limit.
  608. $more = (count($temp) === $limit) ? true : false;
  609. // Merge the results.
  610. $results = array_merge($results, $temp);
  611. }
  612. // Check if there are any excluded terms to deal with.
  613. if (count($excluded))
  614. {
  615. // Remove any results that match excluded terms.
  616. for ($i = 0, $c = count($results); $i < $c; $i++)
  617. {
  618. if (in_array($results[$i]->link_id, $excluded))
  619. {
  620. unset($results[$i]);
  621. }
  622. }
  623. // Reset the array keys.
  624. $results = array_values($results);
  625. }
  626. /*
  627. * If we are ordering by relevance we have to add up the relevance
  628. * scores that are contained in the ordering field.
  629. */
  630. if ($ordering === 'm.weight')
  631. {
  632. // Iterate through the set to extract the unique items.
  633. for ($i = 0, $c = count($results); $i < $c; $i++)
  634. {
  635. // Add the total weights for all included search terms.
  636. if (isset($sorted[$results[$i]->link_id]))
  637. {
  638. $sorted[$results[$i]->link_id] += (float) $results[$i]->ordering;
  639. }
  640. else
  641. {
  642. $sorted[$results[$i]->link_id] = (float) $results[$i]->ordering;
  643. }
  644. }
  645. }
  646. /*
  647. * If we are ordering by start date we have to add convert the
  648. * dates to unix timestamps.
  649. */
  650. elseif ($ordering === 'l.start_date')
  651. {
  652. // Iterate through the set to extract the unique items.
  653. for ($i = 0, $c = count($results); $i < $c; $i++)
  654. {
  655. if (!isset($sorted[$results[$i]->link_id]))
  656. {
  657. $sorted[$results[$i]->link_id] = strtotime($results[$i]->ordering);
  658. }
  659. }
  660. }
  661. /*
  662. * If we are not ordering by relevance or date, we just have to add
  663. * the unique items to the set.
  664. */
  665. else
  666. {
  667. // Iterate through the set to extract the unique items.
  668. for ($i = 0, $c = count($results); $i < $c; $i++)
  669. {
  670. if (!isset($sorted[$results[$i]->link_id]))
  671. {
  672. $sorted[$results[$i]->link_id] = $results[$i]->ordering;
  673. }
  674. }
  675. }
  676. // Sort the results.
  677. natcasesort($items);
  678. if ($direction === 'DESC')
  679. {
  680. $items = array_reverse($items, true);
  681. }
  682. /*
  683. * If the query contains just optional search terms and we have
  684. * enough items for the page, we can stop here.
  685. */
  686. if (empty($this->requiredTerms))
  687. {
  688. // If we need more items and they're available, make another pass.
  689. if ($more && count($sorted) < ($this->getState('list.start') + $this->getState('list.limit')))
  690. {
  691. // Increment the batch starting point and continue.
  692. $start += $limit;
  693. continue;
  694. }
  695. // Push the results into cache.
  696. $this->store($store, $sorted);
  697. // Return the requested set.
  698. return array_slice($this->retrieve($store), (int) $this->getState('list.start'), (int) $this->getState('list.limit'), true);
  699. }
  700. /*
  701. * The query contains required search terms so we have to iterate
  702. * over the items and remove any items that do not match all of the
  703. * required search terms. This is one of the most expensive steps
  704. * because a required token could theoretically eliminate all of
  705. * current terms which means we would have to loop through all of
  706. * the possibilities.
  707. */
  708. foreach ($this->requiredTerms as $token => $required)
  709. {
  710. // Create a storage key for this set.
  711. $setId = $this->getStoreId('getResultsData:required:' . serialize(array_values($required)) . ':' . $start . ':' . $limit);
  712. // Use the cached data if possible.
  713. if ($this->retrieve($setId))
  714. {
  715. $reqTemp = $this->retrieve($setId);
  716. }
  717. // Check if the token was matched.
  718. elseif (empty($required))
  719. {
  720. return null;
  721. }
  722. // Load the data from the database.
  723. else
  724. {
  725. // Setup containers in case we have to make multiple passes.
  726. $reqStart = 0;
  727. $reqTemp = array();
  728. do
  729. {
  730. // Get the map table suffix.
  731. $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
  732. // Adjust the query to join on the appropriate mapping table.
  733. $query = clone($base);
  734. $query->join('INNER', $this->_db->quoteName('#__finder_links_terms' . $suffix) . ' AS m ON m.link_id = l.link_id')
  735. ->where('m.term_id IN (' . implode(',', $required) . ')');
  736. // Load the results from the database.
  737. $this->_db->setQuery($query, $reqStart, $limit);
  738. $temp = $this->_db->loadObjectList('link_id');
  739. // Set the required token more flag to true if the set equal the limit.
  740. $reqMore = (count($temp) === $limit) ? true : false;
  741. // Merge the matching set for this token.
  742. $reqTemp = $reqTemp + $temp;
  743. // Increment the term offset.
  744. $reqStart += $limit;
  745. }
  746. while ($reqMore == true);
  747. // Store this set in cache.
  748. $this->store($setId, $reqTemp);
  749. }
  750. // Remove any items that do not match the required term.
  751. $sorted = array_intersect_key($sorted, $reqTemp);
  752. }
  753. // If we need more items and they're available, make another pass.
  754. if ($more && count($sorted) < ($this->getState('list.start') + $this->getState('list.limit')))
  755. {
  756. // Increment the batch starting point.
  757. $start += $limit;
  758. // Merge the found items.
  759. $items = array_merge($items, $sorted);
  760. continue;
  761. }
  762. // Otherwise, end the loop.
  763. else
  764. {
  765. // Set the found items.
  766. $items = $sorted;
  767. $more = false;
  768. }
  769. // End do-while loop.
  770. }
  771. while ($more === true);
  772. // Push the results into cache.
  773. $this->store($store, $items);
  774. // Return the requested set.
  775. return array_slice($this->retrieve($store), (int) $this->getState('list.start'), (int) $this->getState('list.limit'), true);
  776. }
  777. /**
  778. * Method to get an array of link ids that match excluded terms.
  779. *
  780. * @return array An array of links ids.
  781. *
  782. * @since 2.5
  783. * @throws Exception on database error.
  784. */
  785. protected function getExcludedLinkIds()
  786. {
  787. // Check if the search query has excluded terms.
  788. if (empty($this->excludedTerms))
  789. {
  790. return array();
  791. }
  792. // Get the store id.
  793. $store = $this->getStoreId('getExcludedLinkIds', false);
  794. // Use the cached data if possible.
  795. if ($this->retrieve($store))
  796. {
  797. return $this->retrieve($store);
  798. }
  799. // Initialize containers.
  800. $links = array();
  801. $maps = array();
  802. /*
  803. * Iterate through the excluded search terms and group them by mapping
  804. * table suffix. This ensures that we never have to do more than 16
  805. * queries to get a batch. This may seem like a lot but it is rarely
  806. * anywhere near 16 because of the improved mapping algorithm.
  807. */
  808. foreach ($this->excludedTerms as $token => $id)
  809. {
  810. // Get the mapping table suffix.
  811. $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
  812. // Initialize the mapping group.
  813. if (!array_key_exists($suffix, $maps))
  814. {
  815. $maps[$suffix] = array();
  816. }
  817. // Add the terms to the mapping group.
  818. $maps[$suffix][] = (int) $id;
  819. }
  820. /*
  821. * Iterate through the mapping groups and load the excluded links ids
  822. * from each mapping table.
  823. */
  824. // Create a new query object.
  825. $db = $this->getDbo();
  826. $query = $db->getQuery(true);
  827. foreach ($maps as $suffix => $ids)
  828. {
  829. // Create the query to get the links ids.
  830. $query->clear()
  831. ->select('link_id')
  832. ->from($db->quoteName('#__finder_links_terms' . $suffix))
  833. ->where($db->quoteName('term_id') . ' IN (' . implode(',', $ids) . ')')
  834. ->group($db->quoteName('link_id'));
  835. // Load the link ids from the database.
  836. $db->setQuery($query);
  837. $temp = $db->loadColumn();
  838. // Merge the link ids.
  839. $links = array_merge($links, $temp);
  840. }
  841. // Sanitize the link ids.
  842. $links = array_unique($links);
  843. JArrayHelper::toInteger($links);
  844. // Push the link ids into cache.
  845. $this->store($store, $links);
  846. return $links;
  847. }
  848. /**
  849. * Method to get a subquery for filtering link ids mapped to specific
  850. * terms ids.
  851. *
  852. * @param array $terms An array of search term ids.
  853. *
  854. * @return JDatabaseQuery A database object.
  855. *
  856. * @since 2.5
  857. */
  858. protected function getTermsQuery($terms)
  859. {
  860. // Create the SQL query to get the matching link ids.
  861. // TODO: Impact of removing SQL_NO_CACHE?
  862. $db = $this->getDbo();
  863. $query = $db->getQuery(true)
  864. ->select('SQL_NO_CACHE link_id')
  865. ->from('#__finder_links_terms')
  866. ->where('term_id IN (' . implode(',', $terms) . ')');
  867. return $query;
  868. }
  869. /**
  870. * Method to get a store id based on model the configuration state.
  871. *
  872. * This is necessary because the model is used by the component and
  873. * different modules that might need different sets of data or different
  874. * ordering requirements.
  875. *
  876. * @param string $id An identifier string to generate the store id. [optional]
  877. * @param boolean $page True to store the data paged, false to store all data. [optional]
  878. *
  879. * @return string A store id.
  880. *
  881. * @since 2.5
  882. */
  883. protected function getStoreId($id = '', $page = true)
  884. {
  885. // Get the query object.
  886. $query = $this->getQuery();
  887. // Add the search query state.
  888. $id .= ':' . $query->input;
  889. $id .= ':' . $query->language;
  890. $id .= ':' . $query->filter;
  891. $id .= ':' . serialize($query->filters);
  892. $id .= ':' . $query->date1;
  893. $id .= ':' . $query->date2;
  894. $id .= ':' . $query->when1;
  895. $id .= ':' . $query->when2;
  896. if ($page)
  897. {
  898. // Add the list state for page specific data.
  899. $id .= ':' . $this->getState('list.start');
  900. $id .= ':' . $this->getState('list.limit');
  901. $id .= ':' . $this->getState('list.ordering');
  902. $id .= ':' . $this->getState('list.direction');
  903. }
  904. return parent::getStoreId($id);
  905. }
  906. /**
  907. * Method to auto-populate the model state. Calling getState in this method will result in recursion.
  908. *
  909. * @param string $ordering An optional ordering field. [optional]
  910. * @param string $direction An optional direction. [optional]
  911. *
  912. * @return void
  913. *
  914. * @since 2.5
  915. */
  916. protected function populateState($ordering = null, $direction = null)
  917. {
  918. // Get the configuration options.
  919. $app = JFactory::getApplication();
  920. $input = $app->input;
  921. $params = $app->getParams();
  922. $user = JFactory::getUser();
  923. $filter = JFilterInput::getInstance();
  924. $this->setState('filter.language', JLanguageMultilang::isEnabled());
  925. // Setup the stemmer.
  926. if ($params->get('stem', 1) && $params->get('stemmer', 'porter_en'))
  927. {
  928. FinderIndexerHelper::$stemmer = FinderIndexerStemmer::getInstance($params->get('stemmer', 'porter_en'));
  929. }
  930. $request = $input->request;
  931. $options = array();
  932. // Get the query string.
  933. $options['input'] = !is_null($request->get('q')) ? $request->get('q', '', 'string') : $params->get('q');
  934. $options['input'] = $filter->clean($options['input'], 'string');
  935. // Get the empty query setting.
  936. $options['empty'] = $params->get('allow_empty_query', 0);
  937. // Get the query language.
  938. $options['language'] = !is_null($request->get('l')) ? $request->get('l', '', 'cmd') : $params->get('l');
  939. $options['language'] = $filter->clean($options['language'], 'cmd');
  940. // Get the static taxonomy filters.
  941. $options['filter'] = !is_null($request->get('f')) ? $request->get('f', '', 'int') : $params->get('f');
  942. $options['filter'] = $filter->clean($options['filter'], 'int');
  943. // Get the dynamic taxonomy filters.
  944. $options['filters'] = !is_null($request->get('t', '', 'array')) ? $request->get('t', '', 'array') : $params->get('t');
  945. $options['filters'] = $filter->clean($options['filters'], 'array');
  946. JArrayHelper::toInteger($options['filters']);
  947. // Get the start date and start date modifier filters.
  948. $options['date1'] = !is_null($request->get('d1')) ? $request->get('d1', '', 'string') : $params->get('d1');
  949. $options['date1'] = $filter->clean($options['date1'], 'string');
  950. $options['when1'] = !is_null($request->get('w1')) ? $request->get('w1', '', 'string') : $params->get('w1');
  951. $options['when1'] = $filter->clean($options['when1'], 'string');
  952. // Get the end date and end date modifier filters.
  953. $options['date2'] = !is_null($request->get('d2')) ? $request->get('d2', '', 'string') : $params->get('d2');
  954. $options['date2'] = $filter->clean($options['date2'], 'string');
  955. $options['when2'] = !is_null($request->get('w2')) ? $request->get('w2', '', 'string') : $params->get('w2');
  956. $options['when2'] = $filter->clean($options['when2'], 'string');
  957. // Load the query object.
  958. $this->query = new FinderIndexerQuery($options);
  959. // Load the query token data.
  960. $this->excludedTerms = $this->query->getExcludedTermIds();
  961. $this->includedTerms = $this->query->getIncludedTermIds();
  962. $this->requiredTerms = $this->query->getRequiredTermIds();
  963. // Load the list state.
  964. $this->setState('list.start', $input->get('limitstart', 0, 'uint'));
  965. $this->setState('list.limit', $input->get('limit', $app->getCfg('list_limit', 20), 'uint'));
  966. // Load the sort ordering.
  967. $order = $params->get('sort_order', 'relevance');
  968. switch ($order)
  969. {
  970. case 'date':
  971. $this->setState('list.ordering', 'l.start_date');
  972. break;
  973. case 'price':
  974. $this->setState('list.ordering', 'l.list_price');
  975. break;
  976. case ($order == 'relevance' && !empty($this->includedTerms)):
  977. $this->setState('list.ordering', 'm.weight');
  978. break;
  979. default:
  980. $this->setState('list.ordering', 'l.link_id');
  981. break;
  982. }
  983. // Load the sort direction.
  984. $dirn = $params->get('sort_direction', 'desc');
  985. switch ($dirn)
  986. {
  987. case 'asc':
  988. $this->setState('list.direction', 'ASC');
  989. break;
  990. default:
  991. case 'desc':
  992. $this->setState('list.direction', 'DESC');
  993. break;
  994. }
  995. // Set the match limit.
  996. $this->setState('match.limit', 1000);
  997. // Load the parameters.
  998. $this->setState('params', $params);
  999. // Load the user state.
  1000. $this->setState('user.id', (int) $user->get('id'));
  1001. $this->setState('user.groups', $user->getAuthorisedViewLevels());
  1002. }
  1003. /**
  1004. * Method to retrieve data from cache.
  1005. *
  1006. * @param string $id The cache store id.
  1007. * @param boolean $persistent Flag to enable the use of external cache. [optional]
  1008. *
  1009. * @return mixed The cached data if found, null otherwise.
  1010. *
  1011. * @since 2.5
  1012. */
  1013. protected function retrieve($id, $persistent = true)
  1014. {
  1015. $data = null;
  1016. // Use the internal cache if possible.
  1017. if (isset($this->cache[$id]))
  1018. {
  1019. return $this->cache[$id];
  1020. }
  1021. // Use the external cache if data is persistent.
  1022. if ($persistent)
  1023. {
  1024. $data = JFactory::getCache($this->context, 'output')->get($id);
  1025. $data = $data ? unserialize($data) : null;
  1026. }
  1027. // Store the data in internal cache.
  1028. if ($data)
  1029. {
  1030. $this->cache[$id] = $data;
  1031. }
  1032. return $data;
  1033. }
  1034. /**
  1035. * Method to store data in cache.
  1036. *
  1037. * @param string $id The cache store id.
  1038. * @param mixed $data The data to cache.
  1039. * @param boolean $persistent Flag to enable the use of external cache. [optional]
  1040. *
  1041. * @return boolean True on success, false on failure.
  1042. *
  1043. * @since 2.5
  1044. */
  1045. protected function store($id, $data, $persistent = true)
  1046. {
  1047. // Store the data in internal cache.
  1048. $this->cache[$id] = $data;
  1049. // Store the data in external cache if data is persistent.
  1050. if ($persistent)
  1051. {
  1052. return JFactory::getCache($this->context, 'output')->store(serialize($data), $id);
  1053. }
  1054. return true;
  1055. }
  1056. }