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