diff --git a/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php b/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php new file mode 100644 index 0000000..6c194d4 --- /dev/null +++ b/core/modules/node/lib/Drupal/node/Plugin/Search/NodeSearch.php @@ -0,0 +1,336 @@ +get('database'), + $container->get('plugin.manager.entity'), + $container->get('module_handler'), + $container->get('config.factory')->get('search.settings'), + $container->get('keyvalue')->get('state') + ); + } + + /** + * Constructs a \Drupal\node\Plugin\Search\NodeSearch object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param array $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Database\Connection $database + * A database connection object. + * @param \Drupal\Core\Entity\EntityManager $entity_manager + * An entity manager object. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * A module manager object. + * @param \Drupal\Core\Config\Config $search_settings + * A config object for 'search.settings'. + * @param \Drupal\Core\KeyValueStore\KeyValueStoreInterface $state + * The Drupal state object used to set 'node.cron_last'. + */ + public function __construct(array $configuration, $plugin_id, array $plugin_definition, Connection $database, EntityManager $entity_manager, ModuleHandlerInterface $module_handler, Config $search_settings, KeyValueStoreInterface $state) { + $this->database = $database; + $this->entityManager = $entity_manager; + $this->moduleHandler = $module_handler; + $this->searchSettings = $search_settings; + $this->state = $state; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + public function execute() { + $results = array(); + if (!$this->isSearchExecutable()) { + return $results; + } + $keys = $this->keywords; + // Build matching conditions + $query = $this->database + ->select('search_index', 'i', array('target' => 'slave')) + ->extend('Drupal\search\SearchQuery') + ->extend('Drupal\Core\Database\Query\PagerSelectExtender'); + $query->join('node_field_data', 'n', 'n.nid = i.sid'); + $query->condition('n.status', 1) + ->addTag('node_access') + ->searchExpression($keys, 'node'); + + // Insert special keywords. + $query->setOption('type', 'n.type'); + $query->setOption('langcode', 'n.langcode'); + if ($query->setOption('term', 'ti.tid')) { + $query->join('taxonomy_index', 'ti', 'n.nid = ti.nid'); + } + // Only continue if the first pass query matches. + if (!$query->executeFirstPass()) { + return array(); + } + + // Add the ranking expressions. + $this->addNodeRankings($query); + + // Load results. + $find = $query + // Add the language code of the indexed item to the result of the query, + // since the node will be rendered using the respective language. + ->fields('i', array('langcode')) + ->limit(10) + ->execute(); + + $node_storage = $this->entityManager->getStorageController('node'); + $node_render = $this->entityManager->getRenderController('node'); + + foreach ($find as $item) { + // Render the node. + $entities = $node_storage->loadMultiple(array($item->sid)); + // Convert to BCEntity to match node_load_multiple(). + // @todo - remove this when code that receives this object is updated. + $node = $entities[$item->sid]->getBCEntity(); + $build = $node_render->view($node, 'search_result', $item->langcode); + unset($build['#theme']); + $node->rendered = drupal_render($build); + + // Fetch comments for snippet. + $node->rendered .= ' ' . $this->moduleHandler->invoke('comment', 'node_update_index', array($node, $item->langcode)); + + $extra = $this->moduleHandler->invokeAll('node_search_result', array($node, $item->langcode)); + + $language = $this->moduleHandler->invoke('language', 'load', array($item->langcode)); + $uri = $node->uri(); + $username = array( + '#theme' => 'username', + '#account' => $this->entityManager->getStorageController('user')->load($node->uid), + ); + $results[] = array( + 'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE, 'language' => $language))), + 'type' => check_plain($this->entityManager->getStorageController('node_type')->load($node->bundle())->label()), + 'title' => $node->label($item->langcode), + 'user' => drupal_render($username), + 'date' => $node->getChangedTime(), + 'node' => $node, + 'extra' => $extra, + 'score' => $item->calculated_score, + 'snippet' => search_excerpt($keys, $node->rendered, $item->langcode), + 'langcode' => $node->language()->id, + ); + } + return $results; + } + + /** + * Gathers the rankings from the the hook_ranking() implementations. + * + * @param $query + * A query object that has been extended with the Search DB Extender. + */ + protected function addNodeRankings(SelectExtender $query) { + if ($ranking = $this->moduleHandler->invokeAll('ranking')) { + $tables = &$query->getTables(); + foreach ($ranking as $rank => $values) { + // @todo - move rank out of drupal variables. + if ($node_rank = variable_get('node_rank_' . $rank, 0)) { + // If the table defined in the ranking isn't already joined, then add it. + if (isset($values['join']) && !isset($tables[$values['join']['alias']])) { + $query->addJoin($values['join']['type'], $values['join']['table'], $values['join']['alias'], $values['join']['on']); + } + $arguments = isset($values['arguments']) ? $values['arguments'] : array(); + $query->addScore($values['score'], $arguments, $node_rank); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function updateIndex() { + $limit = (int) $this->searchSettings->get('index.cron_limit'); + + $result = $this->database->queryRange("SELECT n.nid FROM {node} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE d.sid IS NULL OR d.reindex <> 0 ORDER BY d.reindex ASC, n.nid ASC", 0, $limit, array(), array('target' => 'slave')); + $nids = $result->fetchCol(); + if (!$nids) { + return; + } + + // The indexing throttle should be aware of the number of language variants + // of a node. + $counter = 0; + $node_storage = $this->entityManager->getStorageController('node'); + foreach ($node_storage->loadMultiple($nids) as $entity) { + // Convert to BCEntity to match node_load_multiple(). + // @todo - remove this when hooks passed this object are updated. + $node = $entity->getBCEntity(); + // Determine when the maximum number of indexable items is reached. + $counter += count($node->getTranslationLanguages()); + if ($counter > $limit) { + break; + } + $this->indexNode($node); + } + } + + /** + * Indexes a single node. + * + * @param \Drupal\Core\Entity\EntityInterface $node + * The node to index. + */ + protected function indexNode(EntityInterface $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(); + + foreach ($languages as $language) { + // Render the node. + $build = $this->moduleHandler->invoke('node', 'view', array($node, 'search_index', $language->id)); + + unset($build['#theme']); + $node->rendered = drupal_render($build); + + $text = '
' . t('%percentage of the site has been indexed.', array('%percentage' => $percentage)) . ' ' . $count . '
'; $form['status'] = array( '#type' => 'details', @@ -124,7 +146,7 @@ public function buildForm(array $form, array &$form_state) { $form['indexing_throttle']['cron_limit'] = array( '#type' => 'select', '#title' => t('Number of items to index per cron run'), - '#default_value' => $config->get('index.cron_limit'), + '#default_value' => $this->searchSettings->get('index.cron_limit'), '#options' => $items, '#description' => t('The maximum number of items indexed in each pass of a cron maintenance task. If necessary, reduce the number of items to prevent timeouts and memory errors while indexing.', array('@cron' => url('admin/reports/status'))) ); @@ -139,7 +161,7 @@ public function buildForm(array $form, array &$form_state) { $form['indexing_settings']['minimum_word_size'] = array( '#type' => 'number', '#title' => t('Minimum word length to index'), - '#default_value' => $config->get('index.minimum_word_size'), + '#default_value' => $this->searchSettings->get('index.minimum_word_size'), '#min' => 1, '#max' => 1000, '#description' => t('The number of characters a word has to be to be indexed. A lower setting means better search result ranking, but also a larger database. Each search query must contain at least one keyword that is this size (or longer).') @@ -147,7 +169,7 @@ public function buildForm(array $form, array &$form_state) { $form['indexing_settings']['overlap_cjk'] = array( '#type' => 'checkbox', '#title' => t('Simple CJK handling'), - '#default_value' => $config->get('index.overlap_cjk'), + '#default_value' => $this->searchSettings->get('index.overlap_cjk'), '#description' => t('Whether to apply a simple Chinese/Japanese/Korean tokenizer based on overlapping sequences. Turn this off if you want to use an external preprocessor for this instead. Does not affect other languages.') ); @@ -160,24 +182,21 @@ public function buildForm(array $form, array &$form_state) { '#type' => 'checkboxes', '#title' => t('Active modules'), '#title_display' => 'invisible', - '#default_value' => $config->get('active_modules'), + '#default_value' => $this->searchSettings->get('active_modules'), '#options' => $module_options, '#description' => t('Choose which search modules are active from the available modules.') ); $form['active']['default_module'] = array( '#title' => t('Default search module'), '#type' => 'radios', - '#default_value' => $config->get('default_module'), + '#default_value' => $this->searchSettings->get('default_module'), '#options' => $module_options, '#description' => t('Choose which search module is the default.') ); - // Per module settings - foreach ($config->get('active_modules') as $module) { - $added_form = $this->moduleHandler->invoke($module, 'search_admin'); - if (is_array($added_form)) { - $form = NestedArray::mergeDeep($form, $added_form); - } + // Per module plugin settings. + foreach ($active_plugins as $plugin) { + $plugin->addToAdminForm($form, $form_state); } // Set #submit so we are sure it's invoked even if one of // the active search modules added its own #submit. @@ -207,17 +226,21 @@ public function validateForm(array &$form, array &$form_state) { */ public function submitForm(array &$form, array &$form_state) { parent::submitForm($form, $form_state); - $config = $this->configFactory->get('search.settings'); // If these settings change, the index needs to be rebuilt. - if (($config->get('index.minimum_word_size') != $form_state['values']['minimum_word_size']) || ($config->get('index.overlap_cjk') != $form_state['values']['overlap_cjk'])) { - $config->set('index.minimum_word_size', $form_state['values']['minimum_word_size']); - $config->set('index.overlap_cjk', $form_state['values']['overlap_cjk']); + if (($this->searchSettings->get('index.minimum_word_size') != $form_state['values']['minimum_word_size']) || ($this->searchSettings->get('index.overlap_cjk') != $form_state['values']['overlap_cjk'])) { + $this->searchSettings->set('index.minimum_word_size', $form_state['values']['minimum_word_size']); + $this->searchSettings->set('index.overlap_cjk', $form_state['values']['overlap_cjk']); drupal_set_message(t('The index will be rebuilt.')); search_reindex(); } - $config->set('index.cron_limit', $form_state['values']['cron_limit']); - $config->set('default_module', $form_state['values']['default_module']); + $this->searchSettings->set('index.cron_limit', $form_state['values']['cron_limit']); + $this->searchSettings->set('default_module', $form_state['values']['default_module']); + + // Handle per-plugin submission logic. + foreach ($this->searchPluginManager->getActivePlugins() as $plugin) { + $plugin->submitAdminForm($form, $form_state); + } // Check whether we are resetting the values. if ($form_state['triggering_element']['#value'] == t('Reset to defaults')) { @@ -226,12 +249,12 @@ public function submitForm(array &$form, array &$form_state) { else { $new_modules = array_filter($form_state['values']['active_modules']); } - if ($config->get('active_modules') != $new_modules) { - $config->set('active_modules', $new_modules); + if ($this->searchSettings->get('active_modules') != $new_modules) { + $this->searchSettings->set('active_modules', $new_modules); drupal_set_message(t('The active search modules have been changed.')); $this->state->set('menu_rebuild_needed', TRUE); } - $config->save(); + $this->searchSettings->save(); } /** @@ -242,4 +265,5 @@ public function searchAdminReindexSubmit(array $form, array &$form_state) { // send the user to the confirmation page $form_state['redirect'] = 'admin/config/search/settings/reindex'; } + } diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php b/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php new file mode 100644 index 0000000..c4cc34d --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/SearchInterface.php @@ -0,0 +1,146 @@ +get('index.cron_limit') (see + * example below). Also, since the cron run could time out and abort in the + * middle of your run, you should update your module's internal bookkeeping on + * when items have last been indexed as you go rather than waiting to the end + * of indexing. + */ + public function updateIndex(); + + /** + * Take action when the search index is going to be rebuilt. + * + * Modules that use updateIndex() should update their indexing + * bookkeeping so that it starts from scratch the next time updateIndex() + * is called. + */ + public function resetIndex(); + + /** + * Report the status of indexing. + * + * The core search module only invokes this method on active module plugins. + * Implementing modules do not need to check whether they are active when + * calculating their return values. + * + * @return + * An associative array with the key-value pairs: + * - remaining: The number of items left to index. + * - total: The total number of items to index. + */ + public function indexStatus(); + + /** + * Add elements to the search settings form. + * + * The core search module only invokes this method on active module plugins. + * + * @param $form + * Nested array of form elements that comprise the form. + * @param $form_state + * A keyed array containing the current state of the form. The arguments + * that drupal_get_form() was originally called with are available in the + * array $form_state['build_info']['args']. + */ + public function addToAdminForm(array &$form, array &$form_state); + + /** + * Handle any submission for elements on the search settings form. + * + * The core search module only invokes this method on active module plugins. + * + * @param $form + * Nested array of form elements that comprise the form. + * @param $form_state + * A keyed array containing the current state of the form. The arguments + * that drupal_get_form() was originally called with are available in the + * array $form_state['build_info']['args']. + */ + public function submitAdminForm(array &$form, array &$form_state); + +} diff --git a/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php new file mode 100644 index 0000000..d69c4a6 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/Plugin/SearchPluginBase.php @@ -0,0 +1,126 @@ +keywords = (string) $keywords; + $this->searchParams = $params; + $this->searchAttributes = $attributes; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getSearchKeywords() { + return $this->keywords; + } + + /** + * {@inheritdoc} + */ + public function getSearchParams() { + return $this->searchParams; + } + + /** + * {@inheritdoc} + */ + public function getSearchAttributes() { + return $this->searchAttributes; + } + + /** + * {@inheritdoc} + */ + public function isSearchExecutable() { + // Default implementation suitable for plugins that only use keywords. + return !empty($this->keywords); + } + + /** + * {@inheritdoc} + */ + public function buildResults() { + $results = $this->execute(); + return array( + '#theme' => 'search_results', + '#results' => $results, + '#module' => $this->pluginDefinition['module'], + ); + } + + /** + * {@inheritdoc} + */ + public function updateIndex() { + // Empty default implementation. + } + + /** + * {@inheritdoc} + */ + public function resetIndex() { + // Empty default implementation. + } + + /** + * {@inheritdoc} + */ + public function indexStatus() { + // No-op default implementation + return array('remaining' => 0, 'total' => 0); + } + + /** + * {@inheritdoc} + */ + public function addToAdminForm(array &$form, array &$form_state) { + // Empty default implementation. + } + + /** + * {@inheritdoc} + */ + public function submitAdminForm(array &$form, array &$form_state) { + // Empty default implementation. + } + +} diff --git a/core/modules/search/lib/Drupal/search/SearchPluginManager.php b/core/modules/search/lib/Drupal/search/SearchPluginManager.php new file mode 100644 index 0000000..41cd975 --- /dev/null +++ b/core/modules/search/lib/Drupal/search/SearchPluginManager.php @@ -0,0 +1,96 @@ + $namespaces['Drupal\search']); + parent::__construct('Plugin\Search', $namespaces, $annotation_namespaces, 'Drupal\search\Annotation\SearchPlugin'); + + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public function processDefinition(&$definition, $plugin_id) { + parent::processDefinition($definition, $plugin_id); + + // Fill in the provider as default values for missing keys. + $definition += array( + 'title' => $definition['provider'], + 'path' => $definition['provider'], + 'module' => $definition['provider'], + ); + } + + /** + * Returns definitions for active search plugins. + * + * @return \Drupal\search\Plugin\SearchInterface[] + * An array of active search plugins, keyed by their ID. + */ + public function getActivePlugins() { + $plugins = array(); + foreach ($this->getActiveDefinitions() as $plugin_id => $definition) { + $plugins[$plugin_id] = $this->createInstance($plugin_id); + } + return $plugins; + } + + /** + * Returns definitions for active search plugins keyed by their ID. + * + * @return array + * An array of active search plugin definitions, keyed by their ID. + */ + public function getActiveDefinitions() { + $active_definitions = array(); + $active_modules = array_flip($this->configFactory->get('search.settings')->get('active_modules')); + foreach ($this->getDefinitions() as $plugin_id => $definition) { + if (isset($active_modules[$definition['module']])) { + $active_definitions[$plugin_id] = $definition; + } + } + return $active_definitions; + } + + /** + * Returns definitions for active search plugins keyed by their module. + * + * @return array + * An array of active search plugin definitions, keyed by their providing + * module. + */ + public function getActiveDefinitionsByModule() { + $active_definitions = array(); + foreach ($this->getActiveDefinitions() as $definition) { + // Re-key by plugin module name. + $active_definitions[$definition['module']] = $definition; + } + return $active_definitions; + } +} diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php index 5b0efeb..74fb54f 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchAdvancedSearchFormTest.php @@ -29,7 +29,7 @@ function setUp() { $this->node = $this->drupalCreateNode(); // First update the index. This does the initial processing. - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); // Then, run the shutdown function. Testing is a unique case where indexing // and searching has to happen in the same request, so running the shutdown diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php index f45895d..aab4c13 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchCommentCountToggleTest.php @@ -66,7 +66,7 @@ function setUp() { $this->drupalPost('comment/reply/' . $this->searchable_nodes['1 comment']->id(), $edit_comment, t('Save')); // First update the index. This does the initial processing. - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); // Then, run the shutdown function. Testing is a unique case where indexing // and searching has to happen in the same request, so running the shutdown diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php index 2313a95..fc99845 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchConfigSettingsFormTest.php @@ -49,7 +49,7 @@ function setUp() { $edit[$body_key] = l($node->label(), 'node/' . $node->id()) . ' pizza sandwich'; $this->drupalPost('node/' . $node->id() . '/edit', $edit, t('Save and keep published')); - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Enable the search block. diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php index ff822c6..649e410 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchEmbedFormTest.php @@ -46,7 +46,7 @@ function setUp() { $this->node = $this->drupalCreateNode(); - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Set up a dummy initial count of times the form has been submitted. diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php index c52549f..587d773 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchExactTest.php @@ -42,7 +42,7 @@ function testExactQuery() { } // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php index d9ab415..8cbe050 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchMultilingualEntityTest.php @@ -95,7 +95,8 @@ function testIndexingThrottle() { // Index only 4 items per cron run. config('search.settings')->set('index.cron_limit', 4)->save(); // Update the index. This does the initial processing. - node_update_index(); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->updateIndex(); // Run the shutdown function. Testing is a unique case where indexing // and searching has to happen in the same request, so running the shutdown // function manually is needed to finish the indexing process. @@ -104,7 +105,7 @@ function testIndexingThrottle() { // the first has one, the second has two and the third has three language // variants. Indexing the third would exceed the throttle limit, so we // expect that only the first two will be indexed. - $status = module_invoke('node', 'search_status'); + $status = $plugin->indexStatus(); $this->assertEqual($status['remaining'], 1, 'Remaining items after updating the search index is 1.'); } @@ -114,14 +115,17 @@ function testIndexingThrottle() { function testSearchingMultilingualFieldValues() { // Update the index and then run the shutdown method. // See testIndexingThrottle() for further explanation. - node_update_index(); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->updateIndex(); search_update_totals(); foreach ($this->searchable_nodes as $node) { // Each searchable node that we created contains values in the body field // in one or more languages. Let's pick the last language variant from the // body array and execute a search using that as a search keyword. $body_language_variant = end($node->body); - $search_result = node_search_execute($body_language_variant[0]['value']); + $plugin->setSearch($body_language_variant[0]['value'], array(), array()); + // Do the search and assert the results. + $search_result = $plugin->execute(); // See whether we get the same node as a result. $this->assertEqual($search_result[0]['node']->id(), $node->id(), 'The search has resulted the correct node.'); } diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php index 67706cf..8f067f6 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchNodeAccessTest.php @@ -45,7 +45,7 @@ function testPhraseSearchPunctuation() { $node = $this->drupalCreateNode(array('body' => array(array('value' => "The bunny's ears were fluffy.")))); // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php index a8c2e61..8792596 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchPreprocessLangcodeTest.php @@ -46,7 +46,7 @@ function testPreprocessLangcode() { $node = $this->drupalCreateNode(array('body' => array(array()), 'langcode' => 'en')); // First update the index. This does the initial processing. - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); // Then, run the shutdown function. Testing is a unique case where indexing // and searching has to happen in the same request, so running the shutdown @@ -73,7 +73,7 @@ function testPreprocessStemming() { )); // First update the index. This does the initial processing. - node_update_index(); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); // Then, run the shutdown function. Testing is a unique case where indexing // and searching has to happen in the same request, so running the shutdown diff --git a/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php b/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php index 887e997..64d9eb9 100644 --- a/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php +++ b/core/modules/search/lib/Drupal/search/Tests/SearchRankingTest.php @@ -63,7 +63,7 @@ function testRankings() { } // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. @@ -90,16 +90,21 @@ function testRankings() { for ($i = 0; $i < 5; $i ++) { $client->post($stats_path, array(), array('nid' => $nid))->send(); } - // Test each of the possible rankings. + // @todo - comments and views are removed from the array since they are + // broken in core. Those modules expected hook_update_index() to be called + // even though it was only called on modules that implemented a search type. + array_pop($node_ranks); + array_pop($node_ranks); foreach ($node_ranks as $node_rank) { // Disable all relevancy rankings except the one we are testing. foreach ($node_ranks as $var) { variable_set('node_rank_' . $var, $var == $node_rank ? 10 : 0); } - // Do the search and assert the results. - $set = node_search_execute('rocks'); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->setSearch('rocks', array(), array()); + $set = $plugin->execute(); $this->assertEqual($set[0]['node']->id(), $nodes[$node_rank][1]->id(), 'Search ranking "' . $node_rank . '" order.'); } } @@ -143,7 +148,7 @@ function testHTMLRankings() { } // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. @@ -154,7 +159,10 @@ function testHTMLRankings() { foreach ($node_ranks as $node_rank) { variable_set('node_rank_' . $node_rank, 0); } - $set = node_search_execute('rocks'); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->setSearch('rocks', array(), array()); + // Do the search and assert the results. + $set = $plugin->execute(); // Test the ranking of each tag. foreach ($sorted_tags as $tag_rank => $tag) { @@ -173,13 +181,15 @@ function testHTMLRankings() { $node = $this->drupalCreateNode($settings); // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. $this->refreshVariables(); - - $set = node_search_execute('rocks'); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->setSearch('rocks', array(), array()); + // Do the search and assert the results. + $set = $plugin->execute(); // Ranking should always be second to last. $set = array_slice($set, -2, 1); @@ -212,7 +222,7 @@ function testDoubleRankings() { $node = $this->drupalCreateNode($settings); // Update the search index. - module_invoke_all('update_index'); + $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); search_update_totals(); // Refresh variables after the treatment. @@ -227,7 +237,10 @@ function testDoubleRankings() { } // Do the search and assert the results. - $set = node_search_execute('rocks'); + $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); + $plugin->setSearch('rocks', array(), array()); + // Do the search and assert the results. + $set = $plugin->execute(); $this->assertEqual($set[0]['node']->id(), $node->id(), 'Search double ranking order.'); } } diff --git a/core/modules/search/search.api.php b/core/modules/search/search.api.php index 03b45aa..003f84b 100644 --- a/core/modules/search/search.api.php +++ b/core/modules/search/search.api.php @@ -11,42 +11,6 @@ */ /** - * Define a custom search type. - * - * This hook allows a module to tell the Search module that it wishes to - * perform searches on content it defines (custom node types, users, or - * comments for example) when a site search is performed. - * - * In order for the search to do anything, your module must also implement - * hook_search_execute(), which is called when someone requests a search on - * your module's type of content. If you want to have your content indexed - * in the standard search index, your module should also implement - * hook_update_index(). If your search type has settings, you can implement - * hook_search_admin() to add them to the search settings page. You can use - * hook_form_FORM_ID_alter(), with FORM_ID set to 'search_form', to add fields - * to the search form (see node_form_search_form_alter() for an example). - * You can use hook_search_access() to limit access to searching, and - * hook_search_page() to override how search results are displayed. - * - * @return - * Array with optional keys: - * - title: Title for the tab on the search page for this module. Defaults to - * the module name if not given. - * - path: Path component after 'search/' for searching with this module. - * Defaults to the module name if not given. - * - conditions_callback: An implementation of callback_search_conditions(). - * - * @ingroup search - */ -function hook_search_info() { - return array( - 'title' => 'Content', - 'path' => 'node', - 'conditions_callback' => 'callback_search_conditions', - ); -} - -/** * Define access to a custom search routine. * * This hook allows a module to define permissions for a search tab. @@ -58,225 +22,6 @@ function hook_search_access() { } /** - * Take action when the search index is going to be rebuilt. - * - * Modules that use hook_update_index() should update their indexing - * bookkeeping so that it starts from scratch the next time hook_update_index() - * is called. - * - * @ingroup search - */ -function hook_search_reset() { - db_update('search_dataset') - ->fields(array('reindex' => REQUEST_TIME)) - ->condition('type', 'node') - ->execute(); -} - -/** - * Report the status of indexing. - * - * The core search module only invokes this hook on active modules. - * Implementing modules do not need to check whether they are active when - * calculating their return values. - * - * @return - * An associative array with the key-value pairs: - * - remaining: The number of items left to index. - * - total: The total number of items to index. - * - * @ingroup search - */ -function hook_search_status() { - $total = db_query('SELECT COUNT(DISTINCT nid) FROM {node_field_data} WHERE status = 1')->fetchField(); - $remaining = db_query("SELECT COUNT(DISTINCT nid) FROM {node_field_data} n LEFT JOIN {search_dataset} d ON d.type = 'node' AND d.sid = n.nid WHERE n.status = 1 AND d.sid IS NULL OR d.reindex <> 0")->fetchField(); - return array('remaining' => $remaining, 'total' => $total); -} - -/** - * Add elements to the search settings form. - * - * @return - * Form array for the Search settings page at admin/config/search/settings. - * - * @ingroup search - */ -function hook_search_admin() { - // Output form for defining rank factor weights. - $form['content_ranking'] = array( - '#type' => 'details', - '#title' => t('Content ranking'), - ); - $form['content_ranking']['#theme'] = 'node_search_admin'; - $form['content_ranking']['#tree'] = TRUE; - $form['content_ranking']['info'] = array( - '#value' => '' . t('The following numbers control which properties the content search should favor when ordering the results. Higher numbers mean more influence, zero means the property is ignored. Changing these numbers does not require the search index to be rebuilt. Changes take effect immediately.') . '' - ); - - // Note: reversed to reflect that higher number = higher ranking. - $options = drupal_map_assoc(range(0, 10)); - $ranks = config('node.settings')->get('search_rank'); - foreach (module_invoke_all('ranking') as $var => $values) { - $form['content_ranking']['factors'][$var] = array( - '#title' => $values['title'], - '#type' => 'select', - '#options' => $options, - '#default_value' => isset($ranks[$var]) ? $ranks[$var] : 0, - ); - } - - $form['#submit'][] = 'node_search_admin_submit'; - - return $form; -} - -/** - * Execute a search for a set of key words. - * - * Use database API with the 'Drupal\Core\Database\Query\PagerSelectExtender' - * query extension to perform your search. - * - * If your module uses hook_update_index() and search_index() to index its - * items, use table 'search_index' aliased to 'i' as the main table in your - * query, with the 'Drupal\search\SearchQuery' extension. You can join to your - * module's table using the 'i.sid' field, which will contain the $sid values - * you provided to search_index(). Add the main keywords to the query by using - * method searchExpression(). The functions search_expression_extract() and - * search_expression_insert() may also be helpful for adding custom search - * parameters to the search expression. - * - * See node_search_execute() for an example of a module that uses the search - * index, and user_search_execute() for an example that doesn't use the search - * index. - * - * @param $keys - * The search keywords as entered by the user. Defaults to NULL. - * @param $conditions - * (optional) An array of additional conditions, such as filters. Defaults to - * NULL. - * - * @return - * An array of search results. To use the default search result display, each - * item should have the following keys': - * - link: (required) The URL of the found item. - * - type: The type of item (such as the content type). - * - title: (required) The name of the item. - * - user: The author of the item. - * - date: A timestamp when the item was last modified. - * - extra: An array of optional extra information items. - * - snippet: An excerpt or preview to show with the result (can be generated - * with search_excerpt()). - * - language: Language code for the item (usually two characters). - * - * @ingroup search - */ -function hook_search_execute($keys = NULL, $conditions = NULL) { - // Build matching conditions - $query = db_select('search_index', 'i', array('target' => 'slave')) - ->extend('Drupal\search\SearchQuery') - ->extend('Drupal\Core\Database\Query\PagerSelectExtender'); - $query->join('node_field_data', 'n', 'n.nid = i.sid'); - $query - ->condition('n.status', 1) - ->addTag('node_access') - ->searchExpression($keys, 'node'); - - // Insert special keywords. - $query->setOption('type', 'n.type'); - $query->setOption('langcode', 'n.langcode'); - if ($query->setOption('term', 'ti.tid')) { - $query->join('taxonomy_index', 'ti', 'n.nid = ti.nid'); - } - // Only continue if the first pass query matches. - if (!$query->executeFirstPass()) { - return array(); - } - - // Add the ranking expressions. - _node_rankings($query); - - // Load results. - $find = $query - // Add the language code of the indexed item to the result of the query, - // since the node will be rendered using the respective language. - ->fields('i', array('langcode')) - ->limit(10) - ->execute(); - $results = array(); - foreach ($find as $item) { - // Render the node. - $node = node_load($item->sid); - $build = node_view($node, 'search_result', $item->langcode); - unset($build['#theme']); - $node->rendered = drupal_render($build); - - // Fetch comments for snippet. - $node->rendered .= ' ' . module_invoke('comment', 'node_update_index', $node, $item->langcode); - - $extra = module_invoke_all('node_search_result', $node, $item->langcode); - - $language = language_load($item->langcode); - $uri = $node->uri(); - $username = array( - '#theme' => 'username', - '#account' => $node, - ); - $results[] = array( - 'link' => url($uri['path'], array_merge($uri['options'], array('absolute' => TRUE, 'language' => $language))), - 'type' => check_plain(node_get_type_label($node)), - 'title' => $node->label($item->langcode), - 'user' => drupal_render($username), - 'date' => $node->changed, - 'node' => $node, - 'extra' => $extra, - 'score' => $item->calculated_score, - 'snippet' => search_excerpt($keys, $node->rendered, $item->langcode), - 'langcode' => $node->langcode, - ); - } - return $results; -} - -/** - * Override the rendering of search results. - * - * A module that implements hook_search_info() to define a type of search may - * implement this hook in order to override the default theming of its search - * results, which is otherwise themed using theme('search_results'). - * - * Note that by default, theme('search_results') and theme('search_result') - * work together to create an ordered list (OL). So your hook_search_page() - * implementation should probably do this as well. - * - * @param $results - * An array of search results. - * - * @return - * A renderable array, which will render the formatted search results with a - * pager included. - * - * @see search-result.tpl.php - * @see search-results.tpl.php - */ -function hook_search_page($results) { - $output['prefix']['#markup'] = '