Issue #303574 by jhodgdon, douggreen, BlakeLucchesi: Fixed Search Ranking Recency scoring algorithm.
parent
f2f572e2c9
commit
829c436ec1
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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.');
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue