PageRenderTime 49ms CodeModel.GetById 20ms RepoModel.GetById 0ms app.codeStats 0ms

/components/com_finder/models/search.php

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