Source for file search.php

Documentation is available at search.php

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

Documentation generated on Tue, 19 Nov 2013 15:12:42 +0100 by phpDocumentor 1.4.3