diff --git a/core/modules/node/node.install b/core/modules/node/node.install index 4082a49c52c..be6d938b502 100644 --- a/core/modules/node/node.install +++ b/core/modules/node/node.install @@ -165,7 +165,4 @@ function node_uninstall() { // Delete remaining general module variables. \Drupal::state()->delete('node.node_access_needs_rebuild'); - - // Delete any stored state. - \Drupal::state()->delete('node.cron_last'); } diff --git a/core/modules/node/node.module b/core/modules/node/node.module index 99f3aee9f56..d7eeeed69ab 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -671,6 +671,29 @@ function template_preprocess_node(&$variables) { } } +/** + * Implements hook_cron(). + */ +function node_cron() { + // Calculate the oldest and newest node created times, for use in search + // rankings. (Note that field aliases have to be variables passed by + // reference.) + $min_alias = 'min_created'; + $max_alias = 'max_created'; + $result = \Drupal::entityQueryAggregate('node') + ->aggregate('created', 'MIN', NULL, $min_alias) + ->aggregate('created', 'MAX', NULL, $max_alias) + ->execute(); + if (isset($result[0])) { + // Make an array with definite keys and store it in the state system. + $array = array( + 'min_created' => $result[0][$min_alias], + 'max_created' => $result[0][$max_alias], + ); + \Drupal::state()->set('node.min_max_update_time', $array); + } +} + /** * Implements hook_ranking(). */ @@ -693,14 +716,23 @@ function node_ranking() { 'score' => 'n.promote', ), ); - - // Add relevance based on creation or changed date. - if ($node_cron_last = \Drupal::state()->get('node.cron_last')) { + // Add relevance based on updated date, but only if it the scale values have + // been calculated in node_cron(). + if ($node_min_max = \Drupal::state()->get('node.min_max_update_time')) { $ranking['recent'] = array( - 'title' => t('Recently posted'), - // Exponential decay with half-life of 6 months, starting at last indexed node - 'score' => 'POW(2.0, (GREATEST(n.created, n.changed) - :node_cron_last) * 6.43e-8)', - 'arguments' => array(':node_cron_last' => $node_cron_last), + 'title' => t('Recently created'), + 'join' => array( + 'type' => 'LEFT', + 'table' => 'node_field_data', + 'alias' => 'nfd', + 'on' => 'nfd.nid = sid', + ), + // Exponential decay with half life of 14% of the age range of nodes. + 'score' => 'EXP(-5 * (1 - (nfd.created - :node_oldest) / :node_range))', + 'arguments' => array( + ':node_oldest' => $node_min_max['min_created'], + ':node_range' => max($node_min_max['max_created'] - $node_min_max['min_created'], 1), + ), ); } return $ranking; diff --git a/core/modules/node/src/Plugin/Search/NodeSearch.php b/core/modules/node/src/Plugin/Search/NodeSearch.php index 8ebb9ccb0a5..8279d9e525e 100644 --- a/core/modules/node/src/Plugin/Search/NodeSearch.php +++ b/core/modules/node/src/Plugin/Search/NodeSearch.php @@ -15,7 +15,6 @@ use Drupal\Core\Database\Query\SelectExtender; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; -use Drupal\Core\State\StateInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Access\AccessibleInterface; @@ -64,13 +63,6 @@ class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInter */ protected $searchSettings; - /** - * The Drupal state object used to set 'node.cron_last'. - * - * @var \Drupal\Core\State\StateInterface - */ - protected $state; - /** * The Drupal account to use for checking for access to advanced search. * @@ -117,7 +109,6 @@ class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInter $container->get('entity.manager'), $container->get('module_handler'), $container->get('config.factory')->get('search.settings'), - $container->get('state'), $container->get('current_user') ); } @@ -139,17 +130,14 @@ class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInter * A module manager object. * @param \Drupal\Core\Config\Config $search_settings * A config object for 'search.settings'. - * @param \Drupal\Core\State\StateInterface $state - * The Drupal state object used to set 'node.cron_last'. * @param \Drupal\Core\Session\AccountInterface $account * The $account object to use for checking for access to advanced search. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, Config $search_settings, StateInterface $state, AccountInterface $account = NULL) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, Config $search_settings, AccountInterface $account = NULL) { $this->database = $database; $this->entityManager = $entity_manager; $this->moduleHandler = $module_handler; $this->searchSettings = $search_settings; - $this->state = $state; $this->account = $account; parent::__construct($configuration, $plugin_id, $plugin_definition); } @@ -348,10 +336,6 @@ class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInter * The node to index. */ protected function indexNode(NodeInterface $node) { - // Save the changed time of the most recent indexed node, for the search - // results half-life calculation. - $this->state->set('node.cron_last', $node->getChangedTime()); - $languages = $node->getTranslationLanguages(); $node_render = $this->entityManager->getViewBuilder('node'); diff --git a/core/modules/search/src/Tests/SearchRankingTest.php b/core/modules/search/src/Tests/SearchRankingTest.php index af44264d6c6..eb9d9a0b06d 100644 --- a/core/modules/search/src/Tests/SearchRankingTest.php +++ b/core/modules/search/src/Tests/SearchRankingTest.php @@ -35,11 +35,12 @@ class SearchRankingTest extends SearchTestBase { // Create a plugin instance. $this->nodeSearch = entity_load('search_page', 'node_search'); + + // Login with sufficient privileges. + $this->drupalLogin($this->drupalCreateUser(array('post comments', 'skip comment approval', 'create page content', 'administer search'))); } public function testRankings() { - // Login with sufficient privileges. - $this->drupalLogin($this->drupalCreateUser(array('post comments', 'skip comment approval', 'create page content', 'administer search'))); // Add a comment field. $this->container->get('comment.manager')->addDefaultField('node', 'page'); @@ -56,6 +57,8 @@ class SearchRankingTest extends SearchTestBase { )), 'title' => 'Drupal rocks', 'body' => array(array('value' => "Drupal's search rocks")), + // Node is one day old. + 'created' => REQUEST_TIME - 24 * 3600, 'sticky' => 0, 'promote' => 0, ); @@ -70,7 +73,8 @@ class SearchRankingTest extends SearchTestBase { $settings['body'][0]['value'] .= " really rocks"; break; case 'recent': - $settings['created'] = REQUEST_TIME + 3600; + // Node is 1 hour hold. + $settings['created'] = REQUEST_TIME - 3600; break; case 'comments': $settings['comment'][0]['status'] = CommentItemInterface::OPEN; @@ -103,7 +107,7 @@ class SearchRankingTest extends SearchTestBase { // Run cron to update the search index and comment/statistics totals. $this->cronRun(); - // Test that the settings form displays the context ranking section. + // Test that the settings form displays the content ranking section. $this->drupalGet('admin/config/search/pages/manage/node_search'); $this->assertText(t('Content ranking')); @@ -127,6 +131,7 @@ class SearchRankingTest extends SearchTestBase { $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array()); $set = $this->nodeSearch->getPlugin()->execute(); $this->assertEqual($set[0]['node']->id(), $nodes[$node_rank][1]->id(), 'Search ranking "' . $node_rank . '" order.'); + // Clear this ranking for the next test. $edit['rankings_' . $node_rank] = 0; } @@ -138,6 +143,55 @@ class SearchRankingTest extends SearchTestBase { foreach ($node_ranks as $node_rank) { $this->assertTrue($this->xpath('//select[@id="edit-rankings-' . $node_rank . '"]//option[@value="0"]'), 'Select list to prioritize ' . $node_rank . ' for node ranks is visible and set to 0.'); } + + // Try with sticky, then promoted. This is a test for issue + // https://drupal.org/node/771596. + $node_ranks = array( + 'sticky' => 10, + 'promote' => 1, + 'relevance' => 0, + 'recent' => 0, + 'comments' => 0, + 'views' => 0, + ); + $configuration = $this->nodeSearch->getPlugin()->getConfiguration(); + foreach ($node_ranks as $var => $value) { + $configuration['rankings'][$var] = $value; + } + $this->nodeSearch->getPlugin()->setConfiguration($configuration); + $this->nodeSearch->save(); + + // Do the search and assert the results. The sticky node should show up + // first, then the promoted node, then all the rest. + $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array()); + $set = $this->nodeSearch->getPlugin()->execute(); + $this->assertEqual($set[0]['node']->id(), $nodes['sticky'][1]->id(), 'Search ranking for sticky first worked.'); + $this->assertEqual($set[1]['node']->id(), $nodes['promote'][1]->id(), 'Search ranking for promoted second worked.'); + + // Try with recent, then comments. This is a test for issues + // https://drupal.org/node/771596 and https://drupal.org/node/303574. + $node_ranks = array( + 'sticky' => 0, + 'promote' => 0, + 'relevance' => 0, + 'recent' => 10, + 'comments' => 1, + 'views' => 0, + ); + $configuration = $this->nodeSearch->getPlugin()->getConfiguration(); + foreach ($node_ranks as $var => $value) { + $configuration['rankings'][$var] = $value; + } + $this->nodeSearch->getPlugin()->setConfiguration($configuration); + $this->nodeSearch->save(); + + // Do the search and assert the results. The recent node should show up + // first, then the commented node, then all the rest. + $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array()); + $set = $this->nodeSearch->getPlugin()->execute(); + $this->assertEqual($set[0]['node']->id(), $nodes['recent'][1]->id(), 'Search ranking for recent first worked.'); + $this->assertEqual($set[1]['node']->id(), $nodes['comments'][1]->id(), 'Search ranking for comments second worked.'); + } /** @@ -150,9 +204,6 @@ class SearchRankingTest extends SearchTestBase { )); $full_html_format->save(); - // Login with sufficient privileges. - $this->drupalLogin($this->drupalCreateUser(array('create page content'))); - // Test HTML tags with different weights. $sorted_tags = array('h1', 'h2', 'h3', 'h4', 'a', 'h5', 'h6', 'notag'); $shuffled_tags = $sorted_tags; @@ -221,46 +272,4 @@ class SearchRankingTest extends SearchTestBase { $node->delete(); } } - - /** - * Verifies that if we combine two rankings, search still works. - * - * See issue http://drupal.org/node/771596 - */ - function testDoubleRankings() { - // Login with sufficient privileges. - $this->drupalLogin($this->drupalCreateUser(array('skip comment approval', 'create page content'))); - - // Create two nodes that will match the search, one that is sticky. - $settings = array( - 'type' => 'page', - 'title' => 'Drupal rocks', - 'body' => array(array('value' => "Drupal's search rocks")), - ); - $this->drupalCreateNode($settings); - $settings['sticky'] = 1; - $node = $this->drupalCreateNode($settings); - - // Update the search index. - $this->nodeSearch->getPlugin()->updateIndex(); - search_update_totals(); - - // Set up for ranking sticky and lots of comments; make sure others are - // disabled. - $node_ranks = array('sticky', 'promote', 'relevance', 'recent', 'comments', 'views'); - $configuration = $this->nodeSearch->getPlugin()->getConfiguration(); - foreach ($node_ranks as $var) { - $value = ($var == 'sticky' || $var == 'comments') ? 10 : 0; - $configuration['rankings'][$var] = $value; - } - $this->nodeSearch->getPlugin()->setConfiguration($configuration); - $this->nodeSearch->save(); - - // Do the search and assert the results. - $this->nodeSearch->getPlugin()->setSearch('rocks', array(), array()); - // Do the search and assert the results. - $set = $this->nodeSearch->getPlugin()->execute(); - $this->assertEqual($set[0]['node']->id(), $node->id(), 'Search double ranking order.'); - } - }