Issue #303574 by jhodgdon, douggreen, BlakeLucchesi: Fixed Search Ranking Recency scoring algorithm.

8.0.x
Alex Pott 2014-09-12 17:34:41 +01:00
parent f2f572e2c9
commit 829c436ec1
4 changed files with 98 additions and 76 deletions

View File

@ -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');
}

View File

@ -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;

View File

@ -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');

View File

@ -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.');
}
}