PageRenderTime 51ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 0ms

/components/com_finder/models/search.php

https://bitbucket.org/eternaware/joomus
PHP | 1236 lines | 617 code | 180 blank | 439 comment | 80 complexity | 340746914c0a305efb7883dd863be24a MD5 | raw file
Possible License(s): 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. 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. $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. // 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. $query->select('l.link_id');
  193. $query->from($db->quoteName('#__finder_links') . ' AS l');
  194. $query->where($db->quoteName('l.access') . ' IN (' . $groups . ')');
  195. $query->where($db->quoteName('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('(' . $db->quoteName('l.publish_start_date') . ' = ' . $nullDate . ' OR ' . $db->quoteName('l.publish_start_date') . ' <= ' . $nowDate . ')');
  201. $query->where('(' . $db->quoteName('l.publish_end_date') . ' = ' . $nullDate . ' OR ' . $db->quoteName('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. $query->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. $sql = clone($base);
  298. $sql->clear('select');
  299. $sql->select('COUNT(DISTINCT l.link_id)');
  300. // Get the total from the database.
  301. $this->_db->setQuery($sql);
  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. $more = false;
  314. $items = array();
  315. $sorted = array();
  316. $maps = array();
  317. $excluded = $this->getExcludedLinkIds();
  318. /*
  319. * Iterate through the included search terms and group them by mapping
  320. * table suffix. This ensures that we never have to do more than 16
  321. * queries to get a batch. This may seem like a lot but it is rarely
  322. * anywhere near 16 because of the improved mapping algorithm.
  323. */
  324. foreach ($this->includedTerms as $token => $ids)
  325. {
  326. // Get the mapping table suffix.
  327. $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
  328. // Initialize the mapping group.
  329. if (!array_key_exists($suffix, $maps))
  330. {
  331. $maps[$suffix] = array();
  332. }
  333. // Add the terms to the mapping group.
  334. $maps[$suffix] = array_merge($maps[$suffix], $ids);
  335. }
  336. /*
  337. * When the query contains search terms we need to find and process the
  338. * result total iteratively using a do-while loop.
  339. */
  340. do
  341. {
  342. // Create a container for the fetched results.
  343. $results = array();
  344. $more = false;
  345. /*
  346. * Iterate through the mapping groups and load the total from each
  347. * mapping table.
  348. */
  349. foreach ($maps as $suffix => $ids)
  350. {
  351. // Create a storage key for this set.
  352. $setId = $this->getStoreId('getResultsTotal:' . serialize(array_values($ids)) . ':' . $start . ':' . $limit);
  353. // Use the cached data if possible.
  354. if ($this->retrieve($setId))
  355. {
  356. $temp = $this->retrieve($setId);
  357. }
  358. // Load the data from the database.
  359. else
  360. {
  361. // Adjust the query to join on the appropriate mapping table.
  362. $sql = clone($base);
  363. $sql->join('INNER', '#__finder_links_terms' . $suffix . ' AS m ON m.link_id = l.link_id');
  364. $sql->where('m.term_id IN (' . implode(',', $ids) . ')');
  365. // Load the results from the database.
  366. $this->_db->setQuery($sql, $start, $limit);
  367. $temp = $this->_db->loadObjectList();
  368. // Set the more flag to true if any of the sets equal the limit.
  369. $more = (count($temp) === $limit) ? true : false;
  370. // We loaded the data unkeyed but we need it to be keyed for later.
  371. $junk = $temp;
  372. $temp = array();
  373. // Convert to an associative array.
  374. for ($i = 0, $c = count($junk); $i < $c; $i++)
  375. {
  376. $temp[$junk[$i]->link_id] = $junk[$i];
  377. }
  378. // Store this set in cache.
  379. $this->store($setId, $temp);
  380. }
  381. // Merge the results.
  382. $results = array_merge($results, $temp);
  383. }
  384. // Check if there are any excluded terms to deal with.
  385. if (count($excluded))
  386. {
  387. // Remove any results that match excluded terms.
  388. for ($i = 0, $c = count($results); $i < $c; $i++)
  389. {
  390. if (in_array($results[$i]->link_id, $excluded))
  391. {
  392. unset($results[$i]);
  393. }
  394. }
  395. // Reset the array keys.
  396. $results = array_values($results);
  397. }
  398. // Iterate through the set to extract the unique items.
  399. for ($i = 0, $c = count($results); $i < $c; $i++)
  400. {
  401. if (!isset($sorted[$results[$i]->link_id]))
  402. {
  403. $sorted[$results[$i]->link_id] = $results[$i]->ordering;
  404. }
  405. }
  406. /*
  407. * If the query contains just optional search terms and we have
  408. * enough items for the page, we can stop here.
  409. */
  410. if (empty($this->requiredTerms))
  411. {
  412. // If we need more items and they're available, make another pass.
  413. if ($more && count($sorted) < $limit)
  414. {
  415. // Increment the batch starting point and continue.
  416. $start += $limit;
  417. continue;
  418. }
  419. // Push the total into cache.
  420. $this->store($store, min(count($sorted), $limit));
  421. // Return the total.
  422. return $this->retrieve($store);
  423. }
  424. /*
  425. * The query contains required search terms so we have to iterate
  426. * over the items and remove any items that do not match all of the
  427. * required search terms. This is one of the most expensive steps
  428. * because a required token could theoretically eliminate all of
  429. * current terms which means we would have to loop through all of
  430. * the possibilities.
  431. */
  432. foreach ($this->requiredTerms as $token => $required)
  433. {
  434. // Create a storage key for this set.
  435. $setId = $this->getStoreId('getResultsTotal:required:' . serialize(array_values($required)) . ':' . $start . ':' . $limit);
  436. // Use the cached data if possible.
  437. if ($this->retrieve($setId))
  438. {
  439. $reqTemp = $this->retrieve($setId);
  440. }
  441. // Check if the token was matched.
  442. elseif (empty($required))
  443. {
  444. return null;
  445. }
  446. // Load the data from the database.
  447. else
  448. {
  449. // Setup containers in case we have to make multiple passes.
  450. $reqMore = false;
  451. $reqStart = 0;
  452. $reqTemp = array();
  453. do
  454. {
  455. // Get the map table suffix.
  456. $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
  457. // Adjust the query to join on the appropriate mapping table.
  458. $sql = clone($base);
  459. $sql->join('INNER', '#__finder_links_terms' . $suffix . ' AS m ON m.link_id = l.link_id');
  460. $sql->where('m.term_id IN (' . implode(',', $required) . ')');
  461. // Load the results from the database.
  462. $this->_db->setQuery($sql, $reqStart, $limit);
  463. $temp = $this->_db->loadObjectList('link_id');
  464. // Set the required token more flag to true if the set equal the limit.
  465. $reqMore = (count($temp) === $limit) ? true : false;
  466. // Merge the matching set for this token.
  467. $reqTemp = $reqTemp + $temp;
  468. // Increment the term offset.
  469. $reqStart += $limit;
  470. }
  471. while ($reqMore == true);
  472. // Store this set in cache.
  473. $this->store($setId, $reqTemp);
  474. }
  475. // Remove any items that do not match the required term.
  476. $sorted = array_intersect_key($sorted, $reqTemp);
  477. }
  478. // If we need more items and they're available, make another pass.
  479. if ($more && count($sorted) < $limit)
  480. {
  481. // Increment the batch starting point.
  482. $start += $limit;
  483. // Merge the found items.
  484. $items = $items + $sorted;
  485. continue;
  486. }
  487. // Otherwise, end the loop.
  488. {
  489. // Merge the found items.
  490. $items = $items + $sorted;
  491. $more = false;
  492. }
  493. // End do-while loop.
  494. }
  495. while ($more === true);
  496. // Set the total.
  497. $total = count($items);
  498. $total = min($total, $limit);
  499. // Push the total into cache.
  500. $this->store($store, $total);
  501. // Return the total.
  502. return $this->retrieve($store);
  503. }
  504. /**
  505. * Method to get the results for the search query.
  506. *
  507. * @return array An array of result data objects.
  508. *
  509. * @since 2.5
  510. * @throws Exception on database error.
  511. */
  512. protected function getResultsData()
  513. {
  514. // Get the store id.
  515. $store = $this->getStoreId('getResultsData', false);
  516. // Use the cached data if possible.
  517. if ($this->retrieve($store))
  518. {
  519. return $this->retrieve($store);
  520. }
  521. // Get the result ordering and direction.
  522. $ordering = $this->getState('list.ordering', 'l.start_date');
  523. $direction = $this->getState('list.direction', 'DESC');
  524. // Get the base query and add the ordering information.
  525. $base = $this->getListQuery();
  526. $base->select($this->_db->escape($ordering) . ' AS ordering');
  527. $base->order($this->_db->escape($ordering) . ' ' . $this->_db->escape($direction));
  528. /*
  529. * If there are no optional or required search terms in the query, we
  530. * can get the results in one relatively simple database query.
  531. */
  532. if (empty($this->includedTerms))
  533. {
  534. // Get the results from the database.
  535. $this->_db->setQuery($base, (int) $this->getState('list.start'), (int) $this->getState('list.limit'));
  536. $return = $this->_db->loadObjectList('link_id');
  537. // Get a new store id because this data is page specific.
  538. $store = $this->getStoreId('getResultsData', true);
  539. // Push the results into cache.
  540. $this->store($store, $return);
  541. // Return the results.
  542. return $this->retrieve($store);
  543. }
  544. /*
  545. * If there are optional or required search terms in the query, the
  546. * process of getting the results is more complicated.
  547. */
  548. $start = 0;
  549. $limit = (int) $this->getState('match.limit');
  550. $items = array();
  551. $sorted = array();
  552. $maps = array();
  553. $excluded = $this->getExcludedLinkIds();
  554. /*
  555. * Iterate through the included search terms and group them by mapping
  556. * table suffix. This ensures that we never have to do more than 16
  557. * queries to get a batch. This may seem like a lot but it is rarely
  558. * anywhere near 16 because of the improved mapping algorithm.
  559. */
  560. foreach ($this->includedTerms as $token => $ids)
  561. {
  562. // Get the mapping table suffix.
  563. $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
  564. // Initialize the mapping group.
  565. if (!array_key_exists($suffix, $maps))
  566. {
  567. $maps[$suffix] = array();
  568. }
  569. // Add the terms to the mapping group.
  570. $maps[$suffix] = array_merge($maps[$suffix], $ids);
  571. }
  572. /*
  573. * When the query contains search terms we need to find and process the
  574. * results iteratively using a do-while loop.
  575. */
  576. do
  577. {
  578. // Create a container for the fetched results.
  579. $results = array();
  580. $more = false;
  581. /*
  582. * Iterate through the mapping groups and load the results from each
  583. * mapping table.
  584. */
  585. foreach ($maps as $suffix => $ids)
  586. {
  587. // Create a storage key for this set.
  588. $setId = $this->getStoreId('getResultsData:' . serialize(array_values($ids)) . ':' . $start . ':' . $limit);
  589. // Use the cached data if possible.
  590. if ($this->retrieve($setId))
  591. {
  592. $temp = $this->retrieve($setId);
  593. }
  594. // Load the data from the database.
  595. else
  596. {
  597. // Adjust the query to join on the appropriate mapping table.
  598. $sql = clone($base);
  599. $sql->join('INNER', $this->_db->quoteName('#__finder_links_terms' . $suffix) . ' AS m ON m.link_id = l.link_id');
  600. $sql->where('m.term_id IN (' . implode(',', $ids) . ')');
  601. // Load the results from the database.
  602. $this->_db->setQuery($sql, $start, $limit);
  603. $temp = $this->_db->loadObjectList('link_id');
  604. // Store this set in cache.
  605. $this->store($setId, $temp);
  606. // The data is keyed by link_id to ease caching, we don't need it till later.
  607. $temp = array_values($temp);
  608. }
  609. // Set the more flag to true if any of the sets equal the limit.
  610. $more = (count($temp) === $limit) ? true : false;
  611. // Merge the results.
  612. $results = array_merge($results, $temp);
  613. }
  614. // Check if there are any excluded terms to deal with.
  615. if (count($excluded))
  616. {
  617. // Remove any results that match excluded terms.
  618. for ($i = 0, $c = count($results); $i < $c; $i++)
  619. {
  620. if (in_array($results[$i]->link_id, $excluded))
  621. {
  622. unset($results[$i]);
  623. }
  624. }
  625. // Reset the array keys.
  626. $results = array_values($results);
  627. }
  628. /*
  629. * If we are ordering by relevance we have to add up the relevance
  630. * scores that are contained in the ordering field.
  631. */
  632. if ($ordering === 'm.weight')
  633. {
  634. // Iterate through the set to extract the unique items.
  635. for ($i = 0, $c = count($results); $i < $c; $i++)
  636. {
  637. // Add the total weights for all included search terms.
  638. if (isset($sorted[$results[$i]->link_id]))
  639. {
  640. $sorted[$results[$i]->link_id] += (float) $results[$i]->ordering;
  641. }
  642. else
  643. {
  644. $sorted[$results[$i]->link_id] = (float) $results[$i]->ordering;
  645. }
  646. }
  647. }
  648. /*
  649. * If we are ordering by start date we have to add convert the
  650. * dates to unix timestamps.
  651. */
  652. elseif ($ordering === 'l.start_date')
  653. {
  654. // Iterate through the set to extract the unique items.
  655. for ($i = 0, $c = count($results); $i < $c; $i++)
  656. {
  657. if (!isset($sorted[$results[$i]->link_id]))
  658. {
  659. $sorted[$results[$i]->link_id] = strtotime($results[$i]->ordering);
  660. }
  661. }
  662. }
  663. /*
  664. * If we are not ordering by relevance or date, we just have to add
  665. * the unique items to the set.
  666. */
  667. else
  668. {
  669. // Iterate through the set to extract the unique items.
  670. for ($i = 0, $c = count($results); $i < $c; $i++)
  671. {
  672. if (!isset($sorted[$results[$i]->link_id]))
  673. {
  674. $sorted[$results[$i]->link_id] = $results[$i]->ordering;
  675. }
  676. }
  677. }
  678. // Sort the results.
  679. natcasesort($items);
  680. if ($direction === 'DESC')
  681. {
  682. $items = array_reverse($items, true);
  683. }
  684. /*
  685. * If the query contains just optional search terms and we have
  686. * enough items for the page, we can stop here.
  687. */
  688. if (empty($this->requiredTerms))
  689. {
  690. // If we need more items and they're available, make another pass.
  691. if ($more && count($sorted) < ($this->getState('list.start') + $this->getState('list.limit')))
  692. {
  693. // Increment the batch starting point and continue.
  694. $start += $limit;
  695. continue;
  696. }
  697. // Push the results into cache.
  698. $this->store($store, $sorted);
  699. // Return the requested set.
  700. return array_slice($this->retrieve($store), (int) $this->getState('list.start'), (int) $this->getState('list.limit'), true);
  701. }
  702. /*
  703. * The query contains required search terms so we have to iterate
  704. * over the items and remove any items that do not match all of the
  705. * required search terms. This is one of the most expensive steps
  706. * because a required token could theoretically eliminate all of
  707. * current terms which means we would have to loop through all of
  708. * the possibilities.
  709. */
  710. foreach ($this->requiredTerms as $token => $required)
  711. {
  712. // Create a storage key for this set.
  713. $setId = $this->getStoreId('getResultsData:required:' . serialize(array_values($required)) . ':' . $start . ':' . $limit);
  714. // Use the cached data if possible.
  715. if ($this->retrieve($setId))
  716. {
  717. $reqTemp = $this->retrieve($setId);
  718. }
  719. // Check if the token was matched.
  720. elseif (empty($required))
  721. {
  722. return null;
  723. }
  724. // Load the data from the database.
  725. else
  726. {
  727. // Setup containers in case we have to make multiple passes.
  728. $reqMore = false;
  729. $reqStart = 0;
  730. $reqTemp = array();
  731. do
  732. {
  733. // Get the map table suffix.
  734. $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
  735. // Adjust the query to join on the appropriate mapping table.
  736. $sql = clone($base);
  737. $sql->join('INNER', $this->_db->quoteName('#__finder_links_terms' . $suffix) . ' AS m ON m.link_id = l.link_id');
  738. $sql->where('m.term_id IN (' . implode(',', $required) . ')');
  739. // Load the results from the database.
  740. $this->_db->setQuery($sql, $reqStart, $limit);
  741. $temp = $this->_db->loadObjectList('link_id');
  742. // Set the required token more flag to true if the set equal the limit.
  743. $reqMore = (count($temp) === $limit) ? true : false;
  744. // Merge the matching set for this token.
  745. $reqTemp = $reqTemp + $temp;
  746. // Increment the term offset.
  747. $reqStart += $limit;
  748. }
  749. while ($reqMore == true);
  750. // Store this set in cache.
  751. $this->store($setId, $reqTemp);
  752. }
  753. // Remove any items that do not match the required term.
  754. $sorted = array_intersect_key($sorted, $reqTemp);
  755. }
  756. // If we need more items and they're available, make another pass.
  757. if ($more && count($sorted) < ($this->getState('list.start') + $this->getState('list.limit')))
  758. {
  759. // Increment the batch starting point.
  760. $start += $limit;
  761. // Merge the found items.
  762. $items = array_merge($items, $sorted);
  763. continue;
  764. }
  765. // Otherwise, end the loop.
  766. else
  767. {
  768. // Set the found items.
  769. $items = $sorted;
  770. $more = false;
  771. }
  772. // End do-while loop.
  773. }
  774. while ($more === true);
  775. // Push the results into cache.
  776. $this->store($store, $items);
  777. // Return the requested set.
  778. return array_slice($this->retrieve($store), (int) $this->getState('list.start'), (int) $this->getState('list.limit'), true);
  779. }
  780. /**
  781. * Method to get an array of link ids that match excluded terms.
  782. *
  783. * @return array An array of links ids.
  784. *
  785. * @since 2.5
  786. * @throws Exception on database error.
  787. */
  788. protected function getExcludedLinkIds()
  789. {
  790. // Check if the search query has excluded terms.
  791. if (empty($this->excludedTerms))
  792. {
  793. return array();
  794. }
  795. // Get the store id.
  796. $store = $this->getStoreId('getExcludedLinkIds', false);
  797. // Use the cached data if possible.
  798. if ($this->retrieve($store))
  799. {
  800. return $this->retrieve($store);
  801. }
  802. // Initialize containers.
  803. $links = array();
  804. $maps = array();
  805. /*
  806. * Iterate through the excluded search terms and group them by mapping
  807. * table suffix. This ensures that we never have to do more than 16
  808. * queries to get a batch. This may seem like a lot but it is rarely
  809. * anywhere near 16 because of the improved mapping algorithm.
  810. */
  811. foreach ($this->excludedTerms as $token => $id)
  812. {
  813. // Get the mapping table suffix.
  814. $suffix = JString::substr(md5(JString::substr($token, 0, 1)), 0, 1);
  815. // Initialize the mapping group.
  816. if (!array_key_exists($suffix, $maps))
  817. {
  818. $maps[$suffix] = array();
  819. }
  820. // Add the terms to the mapping group.
  821. $maps[$suffix][] = (int) $id;
  822. }
  823. /*
  824. * Iterate through the mapping groups and load the excluded links ids
  825. * from each mapping table.
  826. */
  827. foreach ($maps as $suffix => $ids)
  828. {
  829. // Create a new query object.
  830. $db = $this->getDbo();
  831. $query = $db->getQuery(true);
  832. // Create the query to get the links ids.
  833. $query->select('link_id');
  834. $query->from($db->quoteName('#__finder_links_terms' . $suffix));
  835. $query->where($db->quoteName('term_id') . ' IN (' . implode(',', $ids) . ')');
  836. $query->group($db->quoteName('link_id'));
  837. // Load the link ids from the database.
  838. $db->setQuery($query);
  839. $temp = $db->loadColumn();
  840. // Merge the link ids.
  841. $links = array_merge($links, $temp);
  842. }
  843. // Sanitize the link ids.
  844. $links = array_unique($links);
  845. JArrayHelper::toInteger($links);
  846. // Push the link ids into cache.
  847. $this->store($store, $links);
  848. return $links;
  849. }
  850. /**
  851. * Method to get a subquery for filtering link ids mapped to specific
  852. * terms ids.
  853. *
  854. * @param array $terms An array of search term ids.
  855. *
  856. * @return JDatabaseQuery A database object.
  857. *
  858. * @since 2.5
  859. */
  860. protected function getTermsQuery($terms)
  861. {
  862. // Create the SQL query to get the matching link ids.
  863. //@TODO: Impact of removing SQL_NO_CACHE?
  864. $db = $this->getDbo();
  865. $query = $db->getQuery(true);
  866. $query->select('SQL_NO_CACHE link_id');
  867. $query->from('#__finder_links_terms');
  868. $query->where('term_id IN (' . implode(',', $terms) . ')');
  869. return $query;
  870. }
  871. /**
  872. * Method to get a store id based on model the configuration state.
  873. *
  874. * This is necessary because the model is used by the component and
  875. * different modules that might need different sets of data or different
  876. * ordering requirements.
  877. *
  878. * @param string $id An identifier string to generate the store id. [optional]
  879. * @param boolean $page True to store the data paged, false to store all data. [optional]
  880. *
  881. * @return string A store id.
  882. *
  883. * @since 2.5
  884. */
  885. protected function getStoreId($id = '', $page = true)
  886. {
  887. // Get the query object.
  888. $query = $this->getQuery();
  889. // Add the search query state.
  890. $id .= ':' . $query->input;
  891. $id .= ':' . $query->language;
  892. $id .= ':' . $query->filter;
  893. $id .= ':' . serialize($query->filters);
  894. $id .= ':' . $query->date1;
  895. $id .= ':' . $query->date2;
  896. $id .= ':' . $query->when1;
  897. $id .= ':' . $query->when2;
  898. if ($page)
  899. {
  900. // Add the list state for page specific data.
  901. $id .= ':' . $this->getState('list.start');
  902. $id .= ':' . $this->getState('list.limit');
  903. $id .= ':' . $this->getState('list.ordering');
  904. $id .= ':' . $this->getState('list.direction');
  905. }
  906. return parent::getStoreId($id);
  907. }
  908. /**
  909. * Method to auto-populate the model state. Calling getState in this method will result in recursion.
  910. *
  911. * @param string $ordering An optional ordering field. [optional]
  912. * @param string $direction An optional direction. [optional]
  913. *
  914. * @return void
  915. *
  916. * @since 2.5
  917. */
  918. protected function populateState($ordering = null, $direction = null)
  919. {
  920. // Get the configuration options.
  921. $app = JFactory::getApplication();
  922. $input = $app->input;
  923. $params = $app->getParams();
  924. $user = JFactory::getUser();
  925. $filter = JFilterInput::getInstance();
  926. $this->setState('filter.language', $app->getLanguageFilter());
  927. // Setup the stemmer.
  928. if ($params->get('stem', 1) && $params->get('stemmer', 'porter_en'))
  929. {
  930. FinderIndexerHelper::$stemmer = FinderIndexerStemmer::getInstance($params->get('stemmer', 'porter_en'));
  931. }
  932. $request = $input->request;
  933. $options = array();
  934. // Get the query string.
  935. $options['input'] = !is_null($request->get('q')) ? $request->get('q', '', 'string') : $params->get('q');
  936. $options['input'] = $filter->clean($options['input'], 'string');
  937. // Get the empty query setting.
  938. $options['empty'] = $params->get('allow_empty_query', 0);
  939. // Get the query language.
  940. $options['language'] = !is_null($request->get('l')) ? $request->get('l', '', 'cmd') : $params->get('l');
  941. $options['language'] = $filter->clean($options['language'], 'cmd');
  942. // Get the static taxonomy filters.
  943. $options['filter'] = !is_null($request->get('f')) ? $request->get('f', '', 'int') : $params->get('f');
  944. $options['filter'] = $filter->clean($options['filter'], 'int');
  945. // Get the dynamic taxonomy filters.
  946. $options['filters'] = !is_null($request->get('t')) ? $request->get('t', '', 'array') : $params->get('t');
  947. $options['filters'] = $filter->clean($options['filters'], 'array');
  948. JArrayHelper::toInteger($options['filters']);
  949. // Get the start date and start date modifier filters.
  950. $options['date1'] = !is_null($request->get('d1')) ? $request->get('d1', '', 'string') : $params->get('d1');
  951. $options['date1'] = $filter->clean($options['date1'], 'string');
  952. $options['when1'] = !is_null($request->get('w1')) ? $request->get('w1', '', 'string') : $params->get('w1');
  953. $options['when1'] = $filter->clean($options['when1'], 'string');
  954. // Get the end date and end date modifier filters.
  955. $options['date2'] = !is_null($request->get('d2')) ? $request->get('d2', '', 'string') : $params->get('d2');
  956. $options['date2'] = $filter->clean($options['date2'], 'string');
  957. $options['when2'] = !is_null($request->get('w2')) ? $request->get('w2', '', 'string') : $params->get('w2');
  958. $options['when2'] = $filter->clean($options['when2'], 'string');
  959. // Load the query object.
  960. $this->query = new FinderIndexerQuery($options);
  961. // Load the query token data.
  962. $this->excludedTerms = $this->query->getExcludedTermIds();
  963. $this->includedTerms = $this->query->getIncludedTermIds();
  964. $this->requiredTerms = $this->query->getRequiredTermIds();
  965. // Load the list state.
  966. $this->setState('list.start', $input->get('limitstart', 0, 'uint'));
  967. $this->setState('list.limit', $input->get('limit', $app->getCfg('list_limit', 20), 'uint'));
  968. // Load the sort ordering.
  969. $order = $params->get('sort_order', 'relevance');
  970. switch ($order)
  971. {
  972. case 'date':
  973. $this->setState('list.ordering', 'l.start_date');
  974. break;
  975. case 'price':
  976. $this->setState('list.ordering', 'l.list_price');
  977. break;
  978. default:
  979. case ($order == 'relevance' && !empty($this->includedTerms)):
  980. $this->setState('list.ordering', 'm.weight');
  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. }