diff --git a/core/modules/help_topics/src/Plugin/Search/HelpSearch.php b/core/modules/help_topics/src/Plugin/Search/HelpSearch.php index fc5daa2e8830a2902eb4cf96d699bfea5740dc18..dfd781735ae3ecda296ee0cd2473ebdd461ec400 100644 --- a/core/modules/help_topics/src/Plugin/Search/HelpSearch.php +++ b/core/modules/help_topics/src/Plugin/Search/HelpSearch.php @@ -18,6 +18,7 @@ use Drupal\help_topics\SearchableHelpInterface; use Drupal\search\Plugin\SearchIndexingInterface; use Drupal\search\Plugin\SearchPluginBase; +use Drupal\search\SearchIndexInterface; use Drupal\search\SearchQuery; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -91,6 +92,13 @@ class HelpSearch extends SearchPluginBase implements AccessibleInterface, Search */ protected $helpSectionManager; + /** + * The search index. + * + * @var \Drupal\search\SearchIndexInterface + */ + protected $searchIndex; + /** * {@inheritdoc} */ @@ -105,7 +113,8 @@ public static function create(ContainerInterface $container, array $configuratio $container->get('messenger'), $container->get('current_user'), $container->get('state'), - $container->get('plugin.manager.help_section') + $container->get('plugin.manager.help_section'), + $container->get('search.index') ); } @@ -132,8 +141,10 @@ public static function create(ContainerInterface $container, array $configuratio * The state object. * @param \Drupal\help\HelpSectionManager $help_section_manager * The help section manager. + * @param \Drupal\search\SearchIndexInterface $search_index + * The search index. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, Config $search_settings, LanguageManagerInterface $language_manager, MessengerInterface $messenger, AccountInterface $account, StateInterface $state, HelpSectionManager $help_section_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, Config $search_settings, LanguageManagerInterface $language_manager, MessengerInterface $messenger, AccountInterface $account, StateInterface $state, HelpSectionManager $help_section_manager, SearchIndexInterface $search_index) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->database = $database; $this->searchSettings = $search_settings; @@ -142,6 +153,7 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition $this->account = $account; $this->state = $state; $this->helpSectionManager = $help_section_manager; + $this->searchIndex = $search_index; } /** @@ -327,35 +339,41 @@ public function updateIndex() { $language_list = $this->languageManager->getLanguages(LanguageInterface::STATE_CONFIGURABLE); $section_plugins = []; - foreach ($items as $item) { - $section_plugin_id = $item->section_plugin_id; - if (!isset($section_plugins[$section_plugin_id])) { - $section_plugins[$section_plugin_id] = $this->getSectionPlugin($section_plugin_id); - } + $words = []; + try { + foreach ($items as $item) { + $section_plugin_id = $item->section_plugin_id; + if (!isset($section_plugins[$section_plugin_id])) { + $section_plugins[$section_plugin_id] = $this->getSectionPlugin($section_plugin_id); + } - if (!$section_plugins[$section_plugin_id]) { - $this->removeItemsFromIndex($item->sid); - continue; - } + if (!$section_plugins[$section_plugin_id]) { + $this->removeItemsFromIndex($item->sid); + continue; + } - $section_plugin = $section_plugins[$section_plugin_id]; - search_index_clear($this->getType(), $item->sid); - foreach ($language_list as $langcode => $language) { - $topic = $section_plugin->renderTopicForSearch($item->topic_id, $language); - if ($topic) { - // Index the title plus body text. - $text = '<h1>' . $topic['title'] . '</h1>' . "\n" . $topic['text']; - search_index($this->getType(), $item->sid, $langcode, $text); + $section_plugin = $section_plugins[$section_plugin_id]; + $this->searchIndex->clear($this->getType(), $item->sid); + foreach ($language_list as $langcode => $language) { + $topic = $section_plugin->renderTopicForSearch($item->topic_id, $language); + if ($topic) { + // Index the title plus body text. + $text = '<h1>' . $topic['title'] . '</h1>' . "\n" . $topic['text']; + $words += $this->searchIndex->index($this->getType(), $item->sid, $langcode, $text); + } } } } + finally { + $this->searchIndex->updateWordWeights($words); + } } /** * {@inheritdoc} */ public function indexClear() { - search_index_clear($this->getType()); + $this->searchIndex->clear($this->getType()); } /** @@ -419,7 +437,7 @@ public function updateTopicList() { */ public function markForReindex() { $this->updateTopicList(); - search_mark_for_reindex($this->getType()); + $this->searchIndex->markForReindex($this->getType()); } /** @@ -466,7 +484,7 @@ protected function removeItemsFromIndex($sids) { // Remove items from the search tables individually, as there is no bulk // function to delete items from the search index. foreach ($sids as $sid) { - search_index_clear($this->getType(), $sid); + $this->searchIndex->clear($this->getType(), $sid); } } diff --git a/core/modules/node/node.module b/core/modules/node/node.module index d6e7f2e25cc117387004f6de0b9d923895abc956..9a8d8c999cf66be86da9f7e9241e8854a6f18604 100644 --- a/core/modules/node/node.module +++ b/core/modules/node/node.module @@ -1421,7 +1421,7 @@ function node_configurable_language_delete(ConfigurableLanguageInterface $langua function node_reindex_node_search($nid) { if (\Drupal::moduleHandler()->moduleExists('search')) { // Reindex node context indexed by the node module search plugin. - search_mark_for_reindex('node_search', $nid); + \Drupal::service('search.index')->markForReindex('node_search', $nid); } } diff --git a/core/modules/node/src/Entity/Node.php b/core/modules/node/src/Entity/Node.php index d6c64e2ea3bc360da779de30877435a85ad3da86..fb54e1fb8dbc359378d23a40c010b64a3f1bc744 100644 --- a/core/modules/node/src/Entity/Node.php +++ b/core/modules/node/src/Entity/Node.php @@ -163,9 +163,11 @@ public static function preDelete(EntityStorageInterface $storage, array $entitie parent::preDelete($storage, $entities); // Ensure that all nodes deleted are removed from the search index. - if (\Drupal::moduleHandler()->moduleExists('search')) { + if (\Drupal::hasService('search.index')) { + /** @var \Drupal\search\SearchIndexInterface $search_index */ + $search_index = \Drupal::service('search.index'); foreach ($entities as $entity) { - search_index_clear('node_search', $entity->nid->value); + $search_index->clear('node_search', $entity->nid->value); } } } diff --git a/core/modules/node/src/Plugin/Search/NodeSearch.php b/core/modules/node/src/Plugin/Search/NodeSearch.php index 777d2b8b7a4650903f1afdb0c63ae3cc66429bc5..af4b109d1ddb80fb857b0a9af8f086dd49ca12b0 100644 --- a/core/modules/node/src/Plugin/Search/NodeSearch.php +++ b/core/modules/node/src/Plugin/Search/NodeSearch.php @@ -2,10 +2,12 @@ namespace Drupal\node\Plugin\Search; +use Drupal\Core\Access\AccessibleInterface; use Drupal\Core\Access\AccessResult; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Config\Config; use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Query\Condition; use Drupal\Core\Database\Query\SelectExtender; use Drupal\Core\Database\StatementInterface; use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait; @@ -15,14 +17,13 @@ use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\Core\Access\AccessibleInterface; -use Drupal\Core\Database\Query\Condition; -use Drupal\Core\Render\RendererInterface; use Drupal\node\NodeInterface; use Drupal\search\Plugin\ConfigurableSearchPluginBase; use Drupal\search\Plugin\SearchIndexingInterface; +use Drupal\search\SearchIndexInterface; use Drupal\Search\SearchQuery; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -98,6 +99,13 @@ class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInter */ protected $renderer; + /** + * The search index. + * + * @var \Drupal\search\SearchIndexInterface + */ + protected $searchIndex; + /** * An array of additional rankings from hook_ranking(). * @@ -153,7 +161,8 @@ public static function create(ContainerInterface $container, array $configuratio $container->get('renderer'), $container->get('messenger'), $container->get('current_user'), - $container->get('database.replica') + $container->get('database.replica'), + $container->get('search.index') ); } @@ -184,8 +193,10 @@ public static function create(ContainerInterface $container, array $configuratio * The $account object to use for checking for access to advanced search. * @param \Drupal\Core\Database\Connection|null $database_replica * (Optional) the replica database connection. + * @param \Drupal\search\SearchIndexInterface $search_index + * The search index. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, Config $search_settings, LanguageManagerInterface $language_manager, RendererInterface $renderer, MessengerInterface $messenger, AccountInterface $account = NULL, Connection $database_replica = NULL) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, Connection $database, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, Config $search_settings, LanguageManagerInterface $language_manager, RendererInterface $renderer, MessengerInterface $messenger, AccountInterface $account = NULL, Connection $database_replica = NULL, SearchIndexInterface $search_index = NULL) { $this->database = $database; $this->databaseReplica = $database_replica ?: $database; $this->entityTypeManager = $entity_type_manager; @@ -198,6 +209,11 @@ public function __construct(array $configuration, $plugin_id, $plugin_definition parent::__construct($configuration, $plugin_id, $plugin_definition); $this->addCacheTags(['node_list']); + if (!$search_index) { + @trigger_error('Calling NodeSearch::__construct() without the $search_index argument is deprecated in drupal:8.8.0 and is required in drupal:9.0.0. See https://www.drupal.org/node/3075696', E_USER_DEPRECATED); + $search_index = \Drupal::service('search.index'); + } + $this->searchIndex = $search_index; } /** @@ -478,8 +494,14 @@ public function updateIndex() { } $node_storage = $this->entityTypeManager->getStorage('node'); - foreach ($node_storage->loadMultiple($nids) as $node) { - $this->indexNode($node); + $words = []; + try { + foreach ($node_storage->loadMultiple($nids) as $node) { + $this->indexNode($node, $words); + } + } + finally { + $this->searchIndex->updateWordWeights($words); } } @@ -488,8 +510,10 @@ public function updateIndex() { * * @param \Drupal\node\NodeInterface $node * The node to index. + * @param array $words + * Words that need updating after the index run. */ - protected function indexNode(NodeInterface $node) { + protected function indexNode(NodeInterface $node, array &$words) { $languages = $node->getTranslationLanguages(); $node_render = $this->entityTypeManager->getViewBuilder('node'); @@ -516,7 +540,7 @@ protected function indexNode(NodeInterface $node) { } // Update index, using search index "type" equal to the plugin ID. - search_index($this->getPluginId(), $node->id(), $language->getId(), $text); + $words += $this->searchIndex->index($this->getPluginId(), $node->id(), $language->getId(), $text); } } @@ -526,7 +550,7 @@ protected function indexNode(NodeInterface $node) { public function indexClear() { // All NodeSearch pages share a common search index "type" equal to // the plugin ID. - search_index_clear($this->getPluginId()); + $this->searchIndex->clear($this->getPluginId()); } /** @@ -535,7 +559,7 @@ public function indexClear() { public function markForReindex() { // All NodeSearch pages share a common search index "type" equal to // the plugin ID. - search_mark_for_reindex($this->getPluginId()); + $this->searchIndex->markForReindex($this->getPluginId()); } /** diff --git a/core/modules/search/search.module b/core/modules/search/search.module index 5818f2e80b9b626c06074cc66a416491e35206e6..af1d827b2983ec5df28c3a28d7923cc3d712372f 100644 --- a/core/modules/search/search.module +++ b/core/modules/search/search.module @@ -5,13 +5,11 @@ * Enables site-wide keyword searching. */ -use Drupal\Core\Url; use Drupal\Component\Utility\Html; use Drupal\Component\Utility\Unicode; -use Drupal\Core\Cache\Cache; -use Drupal\Core\Database\Query\Condition; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\Core\Url; /** * Matches all 'N' Unicode character classes (numbers) @@ -140,35 +138,15 @@ function search_preprocess_block(&$variables) { * @param string|null $langcode * (optional) Language code of the item to remove from the search index. If * omitted, all items matching $sid and $type are cleared. + * + * @deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use + * \Drupal\search\SearchIndex::clear() instead. + * + * @see https://www.drupal.org/node/3075696 */ function search_index_clear($type = NULL, $sid = NULL, $langcode = NULL) { - $connection = \Drupal::database(); - $query_index = $connection->delete('search_index'); - $query_dataset = $connection->delete('search_dataset'); - if ($type) { - $query_index->condition('type', $type); - $query_dataset->condition('type', $type); - if ($sid) { - $query_index->condition('sid', $sid); - $query_dataset->condition('sid', $sid); - if ($langcode) { - $query_index->condition('langcode', $langcode); - $query_dataset->condition('langcode', $langcode); - } - } - } - - $query_index->execute(); - $query_dataset->execute(); - - if ($type) { - // Invalidate all render cache items that contain data from this index. - Cache::invalidateTags(['search_index:' . $type]); - } - else { - // Invalidate all render cache items that contain data from any index. - Cache::invalidateTags(['search_index']); - } + @trigger_error("search_index_clear() is deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use \Drupal\search\SearchIndex::clear() instead. See https://www.drupal.org/node/3075696", E_USER_DEPRECATED); + \Drupal::service('search.index')->clear($type, $sid, $langcode); } /** @@ -176,14 +154,17 @@ function search_index_clear($type = NULL, $sid = NULL, $langcode = NULL) { * * This is used during indexing (cron). Words that are dirty have outdated * total counts in the search_total table, and need to be recounted. + * + * @deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use custom + * implementation of \Drupal\search\SearchIndexInterface instead. + * + * @see https://www.drupal.org/node/3075696 */ function search_dirty($word = NULL) { - $dirty = &drupal_static(__FUNCTION__, []); - if ($word !== NULL) { - $dirty[$word] = TRUE; - } - else { - return $dirty; + @trigger_error("search_dirty() is deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use custom implementation of \Drupal\search\SearchIndexInterface instead. See https://www.drupal.org/node/3075696", E_USER_DEPRECATED); + // Keep return result type for backward compatibility. + if ($word === NULL) { + return []; } } @@ -192,14 +173,8 @@ function search_dirty($word = NULL) { * * Fires updateIndex() in the plugins for all indexable active search pages, * and cleans up dirty words. - * - * @see search_dirty() */ function search_cron() { - // We register a shutdown function to ensure that search_total is always up - // to date. - drupal_register_shutdown_function('search_update_totals'); - /** @var $search_page_repository \Drupal\search\SearchPageRepositoryInterface */ $search_page_repository = \Drupal::service('search.search_page_repository'); foreach ($search_page_repository->getIndexableSearchPages() as $entity) { @@ -212,34 +187,14 @@ function search_cron() { * * This function is called on shutdown to ensure that {search_total} is always * up to date (even if cron times out or otherwise fails). + * + * @deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use custom + * implementation of \Drupal\search\SearchIndexInterface instead. + * + * @see https://www.drupal.org/node/3075696 */ function search_update_totals() { - $connection = \Drupal::database(); - $replica = \Drupal::service('database.replica'); - // Update word IDF (Inverse Document Frequency) counts for new/changed words. - foreach (search_dirty() as $word => $dummy) { - // Get total count - $total = $replica->query("SELECT SUM(score) FROM {search_index} WHERE word = :word", [':word' => $word])->fetchField(); - // Apply Zipf's law to equalize the probability distribution. - $total = log10(1 + 1 / (max(1, $total))); - $connection->merge('search_total') - ->key('word', $word) - ->fields(['count' => $total]) - ->execute(); - } - // Find words that were deleted from search_index, but are still in - // search_total. We use a LEFT JOIN between the two tables and keep only the - // rows which fail to join. - $result = $replica->query("SELECT t.word AS realword, i.word FROM {search_total} t LEFT JOIN {search_index} i ON t.word = i.word WHERE i.word IS NULL"); - $or = new Condition('OR'); - foreach ($result as $word) { - $or->condition('word', $word->realword); - } - if (count($or) > 0) { - $connection->delete('search_total') - ->condition($or) - ->execute(); - } + @trigger_error("search_update_totals() is deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use custom implementation of \Drupal\search\SearchIndexInterface instead. See https://www.drupal.org/node/3075696", E_USER_DEPRECATED); } /** @@ -439,137 +394,15 @@ function search_invoke_preprocess(&$text, $langcode = NULL) { * The content of this item. Must be a piece of HTML or plain text. * * @ingroup search + * + * @deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use + * \Drupal\search\SearchIndex::index() instead. + * + * @see https://www.drupal.org/node/3075696 */ function search_index($type, $sid, $langcode, $text) { - $minimum_word_size = \Drupal::config('search.settings')->get('index.minimum_word_size'); - - // Multipliers for scores of words inside certain HTML tags. The weights are - // stored in config so that modules can overwrite the default weights. - // Note: 'a' must be included for link ranking to work. - $tags = \Drupal::config('search.settings')->get('index.tag_weights'); - - // Strip off all ignored tags to speed up processing, but insert space before - // and after them to keep word boundaries. - $text = str_replace(['<', '>'], [' <', '> '], $text); - $text = strip_tags($text, '<' . implode('><', array_keys($tags)) . '>'); - - // Split HTML tags from plain text. - $split = preg_split('/\s*<([^>]+?)>\s*/', $text, -1, PREG_SPLIT_DELIM_CAPTURE); - // Note: PHP ensures the array consists of alternating delimiters and literals - // and begins and ends with a literal (inserting $null as required). - - // Odd/even counter. Tag or no tag. - $tag = FALSE; - // Starting score per word. - $score = 1; - // Accumulator for cleaned up data. - $accum = ' '; - // Stack with open tags. - $tagstack = []; - // Counter for consecutive words. - $tagwords = 0; - // Focus state. - $focus = 1; - - // Accumulator for words for index. - $scored_words = []; - - foreach ($split as $value) { - if ($tag) { - // Increase or decrease score per word based on tag - list($tagname) = explode(' ', $value, 2); - $tagname = mb_strtolower($tagname); - // Closing or opening tag? - if ($tagname[0] == '/') { - $tagname = substr($tagname, 1); - // If we encounter unexpected tags, reset score to avoid incorrect boosting. - if (!count($tagstack) || $tagstack[0] != $tagname) { - $tagstack = []; - $score = 1; - } - else { - // Remove from tag stack and decrement score - $score = max(1, $score - $tags[array_shift($tagstack)]); - } - } - else { - if (isset($tagstack[0]) && $tagstack[0] == $tagname) { - // None of the tags we look for make sense when nested identically. - // If they are, it's probably broken HTML. - $tagstack = []; - $score = 1; - } - else { - // Add to open tag stack and increment score - array_unshift($tagstack, $tagname); - $score += $tags[$tagname]; - } - } - // A tag change occurred, reset counter. - $tagwords = 0; - } - else { - // Note: use of PREG_SPLIT_DELIM_CAPTURE above will introduce empty values - if ($value != '') { - $words = search_index_split($value, $langcode); - foreach ($words as $word) { - // Add word to accumulator - $accum .= $word . ' '; - // Check word length. - if (is_numeric($word) || mb_strlen($word) >= $minimum_word_size) { - if (!isset($scored_words[$word])) { - $scored_words[$word] = 0; - } - $scored_words[$word] += $score * $focus; - // Focus is a decaying value in terms of the amount of unique words up to this point. - // From 100 words and more, it decays, to e.g. 0.5 at 500 words and 0.3 at 1000 words. - $focus = min(1, .01 + 3.5 / (2 + count($scored_words) * .015)); - } - $tagwords++; - // Too many words inside a single tag probably mean a tag was accidentally left open. - if (count($tagstack) && $tagwords >= 15) { - $tagstack = []; - $score = 1; - } - } - } - } - $tag = !$tag; - } - - // Remove the item $sid from the search index, and invalidate the relevant - // cache tags. - search_index_clear($type, $sid, $langcode); - - $connection = \Drupal::database(); - // Insert cleaned up data into dataset - $connection->insert('search_dataset') - ->fields([ - 'sid' => $sid, - 'langcode' => $langcode, - 'type' => $type, - 'data' => $accum, - 'reindex' => 0, - ]) - ->execute(); - - // Insert results into search index - foreach ($scored_words as $word => $score) { - // If a word already exists in the database, its score gets increased - // appropriately. If not, we create a new record with the appropriate - // starting score. - $connection->merge('search_index') - ->keys([ - 'word' => $word, - 'sid' => $sid, - 'langcode' => $langcode, - 'type' => $type, - ]) - ->fields(['score' => $score]) - ->expression('score', 'score + :score', [':score' => $score]) - ->execute(); - search_dirty($word); - } + @trigger_error("search_index() is deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use \Drupal\search\SearchIndex::index() instead. See https://www.drupal.org/node/3075696", E_USER_DEPRECATED); + \Drupal::service('search.index')->index($type, $sid, $langcode, $text); } /** @@ -589,25 +422,15 @@ function search_index($type, $sid, $langcode, $text) { * @param string $langcode * (optional) The language code to clear. If omitted, everything matching * $type and $sid is marked. + * + * @deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use + * \Drupal\search\SearchIndex::markForReindex() instead. + * + * @see https://www.drupal.org/node/3075696 */ function search_mark_for_reindex($type = NULL, $sid = NULL, $langcode = NULL) { - $query = \Drupal::database()->update('search_dataset') - ->fields(['reindex' => REQUEST_TIME]) - // Only mark items that were not previously marked for reindex, so that - // marked items maintain their priority by request time. - ->condition('reindex', 0); - - if ($type) { - $query->condition('type', $type); - if ($sid) { - $query->condition('sid', $sid); - if ($langcode) { - $query->condition('langcode', $langcode); - } - } - } - - $query->execute(); + @trigger_error("search_mark_for_reindex() is deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use \Drupal\search\SearchIndex::markForReindex() instead. See https://www.drupal.org/node/3075696", E_USER_DEPRECATED); + \Drupal::service('search.index')->markForReindex($type, $sid, $langcode); } /** diff --git a/core/modules/search/search.services.yml b/core/modules/search/search.services.yml index 80dfd8fcea19f3d1d645879b98b6137763acebfc..49ec2acb7bdd6f9755efdce735406edfd9312c32 100644 --- a/core/modules/search/search.services.yml +++ b/core/modules/search/search.services.yml @@ -6,3 +6,7 @@ services: search.search_page_repository: class: Drupal\search\SearchPageRepository arguments: ['@config.factory', '@entity_type.manager'] + + search.index: + class: Drupal\search\SearchIndex + arguments: ['@config.factory', '@database','@database.replica', '@cache_tags.invalidator'] diff --git a/core/modules/search/src/Exception/SearchIndexException.php b/core/modules/search/src/Exception/SearchIndexException.php new file mode 100644 index 0000000000000000000000000000000000000000..35d9ae9bfedeac2868bb0a1c3cd6445511d7ebe4 --- /dev/null +++ b/core/modules/search/src/Exception/SearchIndexException.php @@ -0,0 +1,8 @@ +<?php + +namespace Drupal\search\Exception; + +/** + * Exception thrown for search index errors. + */ +class SearchIndexException extends \RuntimeException {} diff --git a/core/modules/search/src/Plugin/SearchIndexingInterface.php b/core/modules/search/src/Plugin/SearchIndexingInterface.php index b35bdbf02cab48faee7c66caa707849ec187bafd..7b92f39c7dc5e218f9643ae3a39c0ce16d61e7eb 100644 --- a/core/modules/search/src/Plugin/SearchIndexingInterface.php +++ b/core/modules/search/src/Plugin/SearchIndexingInterface.php @@ -30,8 +30,8 @@ interface SearchIndexingInterface { * This method is called every cron run if the plugin has been set as * an active search module on the Search settings page * (admin/config/search/pages). It allows your module to add items to the - * built-in search index using search_index(), or to add them to your module's - * own indexing mechanism. + * built-in search index by calling the index() method on the search.index + * service class, or to add them to your module's own indexing mechanism. * * When implementing this method, your module should index content items that * were modified or added since the last run. There is a time limit for cron, @@ -49,10 +49,10 @@ public function updateIndex(); * * When a request is made to clear all items from the search index related to * this plugin, this method will be called. If this plugin uses the default - * search index, this method can call search_index_clear($type) to remove - * indexed items from the search database. + * search index, this method can call clear($type) method on the search.index + * service class to remove indexed items from the search database. * - * @see search_index_clear() + * @see \Drupal\search\SearchIndexInterface::clear() */ public function indexClear(); @@ -61,11 +61,11 @@ public function indexClear(); * * When a request is made to mark all items from the search index related to * this plugin for reindexing, this method will be called. If this plugin uses - * the default search index, this method can call - * search_mark_for_reindex($type) to mark the items in the search database for - * reindexing. + * the default search index, this method can call markForReindex($type) method + * on the search.index service class to mark the items in the search database + * for reindexing. * - * @see search_mark_for_reindex() + * @see \Drupal\search\SearchIndexInterface::markForReindex() */ public function markForReindex(); diff --git a/core/modules/search/src/Plugin/SearchInterface.php b/core/modules/search/src/Plugin/SearchInterface.php index b1421ff0c3f750f6782aa739f4117c934c4670d3..09d1fff41d1a2eae74fbac3c3a3fae3b513cda9f 100644 --- a/core/modules/search/src/Plugin/SearchInterface.php +++ b/core/modules/search/src/Plugin/SearchInterface.php @@ -66,8 +66,8 @@ public function isSearchExecutable(); * The type used by this search plugin in the search index, or NULL if this * plugin does not use the search index. * - * @see search_index() - * @see search_index_clear() + * @see \Drupal\search\SearchIndexInterface::index() + * @see \Drupal\search\SearchIndexInterface::clear() */ public function getType(); diff --git a/core/modules/search/src/SearchIndex.php b/core/modules/search/src/SearchIndex.php new file mode 100644 index 0000000000000000000000000000000000000000..e5655d4974189473883a7e900d2ad6c15f66551b --- /dev/null +++ b/core/modules/search/src/SearchIndex.php @@ -0,0 +1,317 @@ +<?php + +namespace Drupal\search; + +use Drupal\Core\Cache\CacheTagsInvalidatorInterface; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Database\Connection; +use Drupal\Core\Database\Query\Condition; +use Drupal\search\Exception\SearchIndexException; + +/** + * Provides search index management functions. + */ +class SearchIndex implements SearchIndexInterface { + + /** + * The config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $configFactory; + + /** + * The database connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $connection; + + /** + * The database replica connection. + * + * @var \Drupal\Core\Database\Connection + */ + protected $replica; + + /** + * The cache tags invalidator. + * + * @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface + */ + protected $cacheTagsInvalidator; + + /** + * SearchIndex constructor. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Database\Connection $connection + * The database connection. + * @param \Drupal\Core\Database\Connection $replica + * The database replica connection. + * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator + * The cache tags invalidator. + */ + public function __construct(ConfigFactoryInterface $config_factory, Connection $connection, Connection $replica, CacheTagsInvalidatorInterface $cache_tags_invalidator) { + $this->configFactory = $config_factory; + $this->connection = $connection; + $this->replica = $replica; + $this->cacheTagsInvalidator = $cache_tags_invalidator; + } + + /** + * {@inheritdoc} + */ + public function index($type, $sid, $langcode, $text, $update_weights = TRUE) { + $settings = $this->configFactory->get('search.settings'); + $minimum_word_size = $settings->get('index.minimum_word_size'); + + // Keep track of the words that need to have their weights updated. + $current_words = []; + + // Multipliers for scores of words inside certain HTML tags. The weights are + // stored in config so that modules can overwrite the default weights. + // Note: 'a' must be included for link ranking to work. + $tags = $settings->get('index.tag_weights'); + + // Strip off all ignored tags to speed up processing, but insert space + // before and after them to keep word boundaries. + $text = str_replace(['<', '>'], [' <', '> '], $text); + $text = strip_tags($text, '<' . implode('><', array_keys($tags)) . '>'); + + // Split HTML tags from plain text. + $split = preg_split('/\s*<([^>]+?)>\s*/', $text, -1, PREG_SPLIT_DELIM_CAPTURE); + // Note: PHP ensures the array consists of alternating delimiters and + // literals and begins and ends with a literal (inserting $null as + // required). + // Odd/even counter. Tag or no tag. + $tag = FALSE; + // Starting score per word. + $score = 1; + // Accumulator for cleaned up data. + $accum = ' '; + // Stack with open tags. + $tagstack = []; + // Counter for consecutive words. + $tagwords = 0; + // Focus state. + $focus = 1; + + // Accumulator for words for index. + $scored_words = []; + + foreach ($split as $value) { + if ($tag) { + // Increase or decrease score per word based on tag. + list($tagname) = explode(' ', $value, 2); + $tagname = mb_strtolower($tagname); + // Closing or opening tag? + if ($tagname[0] == '/') { + $tagname = substr($tagname, 1); + // If we encounter unexpected tags, reset score to avoid incorrect + // boosting. + if (!count($tagstack) || $tagstack[0] != $tagname) { + $tagstack = []; + $score = 1; + } + else { + // Remove from tag stack and decrement score. + $score = max(1, $score - $tags[array_shift($tagstack)]); + } + } + else { + if (isset($tagstack[0]) && $tagstack[0] == $tagname) { + // None of the tags we look for make sense when nested identically. + // If they are, it's probably broken HTML. + $tagstack = []; + $score = 1; + } + else { + // Add to open tag stack and increment score. + array_unshift($tagstack, $tagname); + $score += $tags[$tagname]; + } + } + // A tag change occurred, reset counter. + $tagwords = 0; + } + else { + // Note: use of PREG_SPLIT_DELIM_CAPTURE above will introduce empty + // values. + if ($value != '') { + $words = search_index_split($value, $langcode); + foreach ($words as $word) { + // Add word to accumulator. + $accum .= $word . ' '; + // Check word length. + if (is_numeric($word) || mb_strlen($word) >= $minimum_word_size) { + if (!isset($scored_words[$word])) { + $scored_words[$word] = 0; + } + $scored_words[$word] += $score * $focus; + // Focus is a decaying value in terms of the amount of unique + // words up to this point. From 100 words and more, it decays, to + // e.g. 0.5 at 500 words and 0.3 at 1000 words. + $focus = min(1, .01 + 3.5 / (2 + count($scored_words) * .015)); + } + $tagwords++; + // Too many words inside a single tag probably mean a tag was + // accidentally left open. + if (count($tagstack) && $tagwords >= 15) { + $tagstack = []; + $score = 1; + } + } + } + } + $tag = !$tag; + } + + // Remove the item $sid from the search index, and invalidate the relevant + // cache tags. + $this->clear($type, $sid, $langcode); + + try { + // Insert cleaned up data into dataset. + $this->connection->insert('search_dataset') + ->fields([ + 'sid' => $sid, + 'langcode' => $langcode, + 'type' => $type, + 'data' => $accum, + 'reindex' => 0, + ]) + ->execute(); + + // Insert results into search index. + foreach ($scored_words as $word => $score) { + // If a word already exists in the database, its score gets increased + // appropriately. If not, we create a new record with the appropriate + // starting score. + $this->connection->merge('search_index') + ->keys([ + 'word' => $word, + 'sid' => $sid, + 'langcode' => $langcode, + 'type' => $type, + ]) + ->fields(['score' => $score]) + ->expression('score', 'score + :score', [':score' => $score]) + ->execute(); + $current_words[$word] = TRUE; + } + } + catch (\Exception $e) { + throw new SearchIndexException("Failed to insert dataset in index for type '$type', sid '$sid' and langcode '$langcode'", 0, $e); + } + finally { + if ($update_weights) { + $this->updateWordWeights($current_words); + } + } + return $current_words; + } + + /** + * {@inheritdoc} + */ + public function clear($type = NULL, $sid = NULL, $langcode = NULL) { + + try { + $query_index = $this->connection->delete('search_index'); + $query_dataset = $this->connection->delete('search_dataset'); + if ($type) { + $query_index->condition('type', $type); + $query_dataset->condition('type', $type); + if ($sid) { + $query_index->condition('sid', $sid); + $query_dataset->condition('sid', $sid); + if ($langcode) { + $query_index->condition('langcode', $langcode); + $query_dataset->condition('langcode', $langcode); + } + } + } + $query_index->execute(); + $query_dataset->execute(); + } + catch (\Exception $e) { + throw new SearchIndexException("Failed to clear index for type '$type', sid '$sid' and langcode '$langcode'", 0, $e); + } + if ($type) { + // Invalidate all render cache items that contain data from this index. + $this->cacheTagsInvalidator->invalidateTags(['search_index:' . $type]); + } + else { + // Invalidate all render cache items that contain data from any index. + $this->cacheTagsInvalidator->invalidateTags(['search_index']); + } + } + + /** + * {@inheritdoc} + */ + public function markForReindex($type = NULL, $sid = NULL, $langcode = NULL) { + + try { + $query = $this->connection->update('search_dataset') + ->fields(['reindex' => REQUEST_TIME]) + // Only mark items that were not previously marked for reindex, so that + // marked items maintain their priority by request time. + ->condition('reindex', 0); + if ($type) { + $query->condition('type', $type); + if ($sid) { + $query->condition('sid', $sid); + if ($langcode) { + $query->condition('langcode', $langcode); + } + } + } + $query->execute(); + } + catch (\Exception $e) { + throw new SearchIndexException("Failed to mark index for re-indexing for type '$type', sid '$sid' and langcode '$langcode'", 0, $e); + } + } + + /** + * {@inheritdoc} + */ + public function updateWordWeights(array $words) { + try { + // Update word IDF (Inverse Document Frequency) counts for new/changed + // words. + $words = array_keys($words); + foreach ($words as $word) { + // Get total count. + $total = $this->replica->query("SELECT SUM(score) FROM {search_index} WHERE word = :word", [':word' => $word]) + ->fetchField(); + // Apply Zipf's law to equalize the probability distribution. + $total = log10(1 + 1 / (max(1, $total))); + $this->connection->merge('search_total') + ->key('word', $word) + ->fields(['count' => $total]) + ->execute(); + } + // Find words that were deleted from search_index, but are still in + // search_total. We use a LEFT JOIN between the two tables and keep only + // the rows which fail to join. + $result = $this->replica->query("SELECT t.word AS realword, i.word FROM {search_total} t LEFT JOIN {search_index} i ON t.word = i.word WHERE i.word IS NULL"); + $or = new Condition('OR'); + foreach ($result as $word) { + $or->condition('word', $word->realword); + } + if (count($or) > 0) { + $this->connection->delete('search_total') + ->condition($or) + ->execute(); + } + } + catch (\Exception $e) { + throw new SearchIndexException("Failed to update totals for index words.", 0, $e); + } + } + +} diff --git a/core/modules/search/src/SearchIndexInterface.php b/core/modules/search/src/SearchIndexInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..6b28d2e1c0eae1bdb26cdd772ae211d5e5b8304e --- /dev/null +++ b/core/modules/search/src/SearchIndexInterface.php @@ -0,0 +1,94 @@ +<?php + +namespace Drupal\search; + +/** + * Provides search index management functions. + * + * @ingroup search + */ +interface SearchIndexInterface { + + /** + * Updates the full-text search index for a particular item. + * + * @param string $type + * The plugin ID or other machine-readable type of this item, + * which should be less than 64 bytes. + * @param int $sid + * An ID number identifying this particular item (e.g., node ID). + * @param string $langcode + * Language code for the language of the text being indexed. + * @param string $text + * The content of this item. Must be a piece of HTML or plain text. + * @param bool $update_weights + * (optional) TRUE if word weights should be updated. FALSE otherwise. + * + * @return string[] + * The words to be updated. + * + * @throws \Drupal\search\Exception\SearchIndexException + * If there is an error indexing the text. + */ + public function index($type, $sid, $langcode, $text, $update_weights = TRUE); + + /** + * Clears either a part of, or the entire search index. + * + * This function is meant for use by search page plugins, or for building a + * user interface that lets users clear all or parts of the search index. + * + * @param string|null $type + * (optional) The plugin ID or other machine-readable type for the items to + * remove from the search index. If omitted, $sid and $langcode are ignored + * and the entire search index is cleared. + * @param int|array|null $sid + * (optional) The ID or array of IDs of the items to remove from the search + * index. If omitted, all items matching $type are cleared, and $langcode + * is ignored. + * @param string|null $langcode + * (optional) Language code of the item to remove from the search index. If + * omitted, all items matching $sid and $type are cleared. + * + * @throws \Drupal\search\Exception\SearchIndexException + * If there is an error clearing the index. + */ + public function clear($type = NULL, $sid = NULL, $langcode = NULL); + + /** + * Changes the timestamp on indexed items to 'now' to force reindexing. + * + * This function is meant for use by search page plugins, or for building a + * user interface that lets users mark all or parts of the search index for + * reindexing. + * + * @param string $type + * (optional) The plugin ID or other machine-readable type of this item. If + * omitted, the entire search index is marked for reindexing, and $sid and + * $langcode are ignored. + * @param int $sid + * (optional) An ID number identifying this particular item (e.g., node ID). + * If omitted, everything matching $type is marked, and $langcode is + * ignored. + * @param string $langcode + * (optional) The language code to mark. If omitted, everything matching + * $type and $sid is marked. + * + * @throws \Drupal\search\Exception\SearchIndexException + * If there is an error marking the index for re-indexing. + */ + public function markForReindex($type = NULL, $sid = NULL, $langcode = NULL); + + /** + * Updates the {search_total} database table. + * + * @param array $words + * An array whose keys are words from self::index() whose total weights + * need to be updated. + * + * @throws \Drupal\search\Exception\SearchIndexException + * If there is an error updating the totals. + */ + public function updateWordWeights(array $words); + +} diff --git a/core/modules/search/src/SearchPageListBuilder.php b/core/modules/search/src/SearchPageListBuilder.php index 95e82ee68dbec4439f059ef55d8c390baf06bda0..c0c0258f2c8203e7b5c5fc83a00051c595d30ea4 100644 --- a/core/modules/search/src/SearchPageListBuilder.php +++ b/core/modules/search/src/SearchPageListBuilder.php @@ -43,6 +43,13 @@ class SearchPageListBuilder extends DraggableListBuilder implements FormInterfac */ protected $searchManager; + /** + * The search index. + * + * @var \Drupal\search\SearchIndexInterface + */ + protected $searchIndex; + /** * The messenger. * @@ -63,12 +70,19 @@ class SearchPageListBuilder extends DraggableListBuilder implements FormInterfac * The factory for configuration objects. * @param \Drupal\Core\Messenger\MessengerInterface $messenger * The messenger. + * @param \Drupal\search\SearchIndexInterface $search_index + * The search index. */ - public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, SearchPluginManager $search_manager, ConfigFactoryInterface $config_factory, MessengerInterface $messenger) { + public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, SearchPluginManager $search_manager, ConfigFactoryInterface $config_factory, MessengerInterface $messenger, SearchIndexInterface $search_index = NULL) { parent::__construct($entity_type, $storage); $this->configFactory = $config_factory; $this->searchManager = $search_manager; $this->messenger = $messenger; + if (!$search_index) { + @trigger_error('Calling SearchPageListBuilder::__construct() without the $search_index argument is deprecated in drupal:8.8.0 and is required in drupal:9.0.0. See https://www.drupal.org/node/3075696', E_USER_DEPRECATED); + $search_index = \Drupal::service('search.index'); + } + $this->searchIndex = $search_index; } /** @@ -80,7 +94,8 @@ public static function createInstance(ContainerInterface $container, EntityTypeI $container->get('entity_type.manager')->getStorage($entity_type->id()), $container->get('plugin.manager.search'), $container->get('config.factory'), - $container->get('messenger') + $container->get('messenger'), + $container->get('search.index') ); } @@ -344,9 +359,9 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $search_settings->set('index.minimum_word_size', $form_state->getValue('minimum_word_size')); $search_settings->set('index.overlap_cjk', $form_state->getValue('overlap_cjk')); // Specifically mark items in the default index for reindexing, since - // these settings are used in the search_index() function. + // these settings are used in the SearchIndex::index() function. $this->messenger->addStatus($this->t('The default search index will be rebuilt.')); - search_mark_for_reindex(); + $this->searchIndex->markForReindex(); } $search_settings diff --git a/core/modules/search/tests/src/Functional/SearchAdvancedSearchFormTest.php b/core/modules/search/tests/src/Functional/SearchAdvancedSearchFormTest.php index 03f0ff75cc6e22affb14379c30539d7a33493aad..7933dfd9b96572d31062c46c042c940e816530a3 100644 --- a/core/modules/search/tests/src/Functional/SearchAdvancedSearchFormTest.php +++ b/core/modules/search/tests/src/Functional/SearchAdvancedSearchFormTest.php @@ -38,11 +38,6 @@ protected function setUp() { // First update the index. This does the initial processing. $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 - // function manually is needed to finish the indexing process. - search_update_totals(); } /** diff --git a/core/modules/search/tests/src/Functional/SearchCommentCountToggleTest.php b/core/modules/search/tests/src/Functional/SearchCommentCountToggleTest.php index 2d7811576507c613286a5e3b9779ed5a5cd4cbf1..43f2a311343d9be773b56cff02c7b50ab2fd4d4d 100644 --- a/core/modules/search/tests/src/Functional/SearchCommentCountToggleTest.php +++ b/core/modules/search/tests/src/Functional/SearchCommentCountToggleTest.php @@ -70,11 +70,6 @@ protected function setUp() { // First update the index. This does the initial processing. $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 - // function manually is needed to finish the indexing process. - search_update_totals(); } /** diff --git a/core/modules/search/tests/src/Functional/SearchCommentTest.php b/core/modules/search/tests/src/Functional/SearchCommentTest.php index 8be5cd8e4a47beebb0b357397758d690dab9716c..3c57db0d06c5f3e43df10e687ae114b9dec56a1e 100644 --- a/core/modules/search/tests/src/Functional/SearchCommentTest.php +++ b/core/modules/search/tests/src/Functional/SearchCommentTest.php @@ -294,7 +294,7 @@ public function setRolePermissions($rid, $access_comments = FALSE, $search_conte */ public function assertCommentAccess($assume_access, $message) { // Invoke search index update. - search_mark_for_reindex('node_search', $this->node->id()); + \Drupal::service('search.index')->markForReindex('node_search', $this->node->id()); $this->cronRun(); // Search for the comment subject. diff --git a/core/modules/search/tests/src/Functional/SearchConfigSettingsFormTest.php b/core/modules/search/tests/src/Functional/SearchConfigSettingsFormTest.php index ad25e63f76268b60cc33422fdd82ca7a5d19428c..f0c9422d6a80ddd978395fe80823b85252a86fcf 100644 --- a/core/modules/search/tests/src/Functional/SearchConfigSettingsFormTest.php +++ b/core/modules/search/tests/src/Functional/SearchConfigSettingsFormTest.php @@ -53,7 +53,6 @@ protected function setUp() { $this->drupalPostForm('node/' . $node->id() . '/edit', $edit, t('Save')); $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); - search_update_totals(); // Enable the search block. $this->drupalPlaceBlock('search_form_block'); diff --git a/core/modules/search/tests/src/Functional/SearchDateIntervalTest.php b/core/modules/search/tests/src/Functional/SearchDateIntervalTest.php index 46edc3bb081f23385702b673e6d6d79248e00443..84c2d38f00c94dc584f01cb5bffdb8f0bf9d48d5 100644 --- a/core/modules/search/tests/src/Functional/SearchDateIntervalTest.php +++ b/core/modules/search/tests/src/Functional/SearchDateIntervalTest.php @@ -56,7 +56,6 @@ protected function setUp() { // Update the index. $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); $plugin->updateIndex(); - search_update_totals(); } /** diff --git a/core/modules/search/tests/src/Functional/SearchEmbedFormTest.php b/core/modules/search/tests/src/Functional/SearchEmbedFormTest.php index af0d9b7b61a56cda490bb31a215dbc49f0cfb9bd..c134093df6f2d408b012cf057633becefa0b9a6e 100644 --- a/core/modules/search/tests/src/Functional/SearchEmbedFormTest.php +++ b/core/modules/search/tests/src/Functional/SearchEmbedFormTest.php @@ -42,7 +42,6 @@ protected function setUp() { $this->node = $this->drupalCreateNode(); $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. $this->submitCount = \Drupal::state()->get('search_embedded_form.submit_count'); diff --git a/core/modules/search/tests/src/Functional/SearchExactTest.php b/core/modules/search/tests/src/Functional/SearchExactTest.php index 94de349ebde54ae9619e0d269e9448302e6cbdda..05ad58cff976ddb195ec544f50e41f77bd8dab24 100644 --- a/core/modules/search/tests/src/Functional/SearchExactTest.php +++ b/core/modules/search/tests/src/Functional/SearchExactTest.php @@ -46,7 +46,6 @@ public function testExactQuery() { // Update the search index. $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); - search_update_totals(); // Refresh variables after the treatment. $this->refreshVariables(); diff --git a/core/modules/search/tests/src/Functional/SearchLanguageTest.php b/core/modules/search/tests/src/Functional/SearchLanguageTest.php index c874df7f66dc794559003f00e34f24c8dea4a99e..d884a4bbc4b11f91a87ffa7f741cd389cadbb08d 100644 --- a/core/modules/search/tests/src/Functional/SearchLanguageTest.php +++ b/core/modules/search/tests/src/Functional/SearchLanguageTest.php @@ -85,7 +85,6 @@ protected function setUp() { // Update the index and then run the shutdown method. $plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); $plugin->updateIndex(); - search_update_totals(); } public function testLanguages() { diff --git a/core/modules/search/tests/src/Functional/SearchMultilingualEntityTest.php b/core/modules/search/tests/src/Functional/SearchMultilingualEntityTest.php index 17b3f06afdb67f087e5ed869480dca5c9932add2..d823b2e63279281d0868216d0916ca5098601b91 100644 --- a/core/modules/search/tests/src/Functional/SearchMultilingualEntityTest.php +++ b/core/modules/search/tests/src/Functional/SearchMultilingualEntityTest.php @@ -5,6 +5,7 @@ use Drupal\Core\Database\Database; use Drupal\field\Entity\FieldStorageConfig; use Drupal\language\Entity\ConfigurableLanguage; +use Drupal\search\SearchIndexInterface; use Drupal\Tests\BrowserTestBase; /** @@ -128,7 +129,8 @@ public function testMultilingualSearch() { // 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. - search_update_totals(); + $search_index = \Drupal::service('search.index'); + assert($search_index instanceof SearchIndexInterface); $this->assertIndexCounts(6, 8, 'after updating partially'); $this->assertDatabaseCounts(2, 0, 'after updating partially'); @@ -140,7 +142,6 @@ public function testMultilingualSearch() { $this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); $this->plugin->updateIndex(); - search_update_totals(); $this->assertIndexCounts(0, 8, 'after updating fully'); $this->assertDatabaseCounts(8, 0, 'after updating fully'); @@ -150,7 +151,6 @@ public function testMultilingualSearch() { $this->assertIndexCounts(8, 8, 'after reindex'); $this->assertDatabaseCounts(8, 0, 'after reindex'); $this->plugin->updateIndex(); - search_update_totals(); // Test search results. @@ -190,13 +190,12 @@ public function testMultilingualSearch() { // Mark one of the nodes for reindexing, using the API function, and // verify indexing status. - search_mark_for_reindex('node_search', $this->searchableNodes[0]->id()); + $search_index->markForReindex('node_search', $this->searchableNodes[0]->id()); $this->assertIndexCounts(1, 8, 'after marking one node to reindex via API function'); // Update the index and verify the totals again. $this->plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); $this->plugin->updateIndex(); - search_update_totals(); $this->assertIndexCounts(0, 8, 'after indexing again'); // Mark one node for reindexing by saving it, and verify indexing status. @@ -227,32 +226,32 @@ public function testMultilingualSearch() { // Add a bogus entry to the search index table using a different search // type. This will not appear in the index status, because it is not // managed by a plugin. - search_index('foo', $this->searchableNodes[0]->id(), 'en', 'some text'); + $search_index->index('foo', $this->searchableNodes[0]->id(), 'en', 'some text'); $this->assertIndexCounts(1, 8, 'after adding a different index item'); // Mark just this "foo" index for reindexing. - search_mark_for_reindex('foo'); + $search_index->markForReindex('foo'); $this->assertIndexCounts(1, 8, 'after reindexing the other search type'); // Mark everything for reindexing. - search_mark_for_reindex(); + $search_index->markForReindex(); $this->assertIndexCounts(8, 8, 'after reindexing everything'); // Clear one item from the index, but with wrong language. $this->assertDatabaseCounts(8, 1, 'before clear'); - search_index_clear('node_search', $this->searchableNodes[0]->id(), 'hu'); + $search_index->clear('node_search', $this->searchableNodes[0]->id(), 'hu'); $this->assertDatabaseCounts(8, 1, 'after clear with wrong language'); // Clear using correct language. - search_index_clear('node_search', $this->searchableNodes[0]->id(), 'en'); + $search_index->clear('node_search', $this->searchableNodes[0]->id(), 'en'); $this->assertDatabaseCounts(7, 1, 'after clear with right language'); // Don't specify language. - search_index_clear('node_search', $this->searchableNodes[1]->id()); + $search_index->clear('node_search', $this->searchableNodes[1]->id()); $this->assertDatabaseCounts(6, 1, 'unspecified language clear'); // Clear everything in 'foo'. - search_index_clear('foo'); + $search_index->clear('foo'); $this->assertDatabaseCounts(6, 0, 'other index clear'); // Clear everything. - search_index_clear(); + $search_index->clear(); $this->assertDatabaseCounts(0, 0, 'complete clear'); } diff --git a/core/modules/search/tests/src/Functional/SearchNodeDiacriticsTest.php b/core/modules/search/tests/src/Functional/SearchNodeDiacriticsTest.php index a529cea9b1192066bc3454be5bb774a051f9dcdb..e83d84b4ad72e3294d840d1723f57f37dc8eca16 100644 --- a/core/modules/search/tests/src/Functional/SearchNodeDiacriticsTest.php +++ b/core/modules/search/tests/src/Functional/SearchNodeDiacriticsTest.php @@ -45,7 +45,6 @@ public function testPhraseSearchPunctuation() { // Update the search index. $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); - search_update_totals(); // Refresh variables after the treatment. $this->refreshVariables(); diff --git a/core/modules/search/tests/src/Functional/SearchNodePunctuationTest.php b/core/modules/search/tests/src/Functional/SearchNodePunctuationTest.php index 0c85714921c537414bda0cbf01d65dfe956f25c5..c0c597ecc5f22f1186487409ad8492cdf72dc52c 100644 --- a/core/modules/search/tests/src/Functional/SearchNodePunctuationTest.php +++ b/core/modules/search/tests/src/Functional/SearchNodePunctuationTest.php @@ -43,7 +43,6 @@ public function testPhraseSearchPunctuation() { // Update the search index. $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); - search_update_totals(); // Refresh variables after the treatment. $this->refreshVariables(); diff --git a/core/modules/search/tests/src/Functional/SearchNodeUpdateAndDeletionTest.php b/core/modules/search/tests/src/Functional/SearchNodeUpdateAndDeletionTest.php index 3ebc584bd393561c17e5079ab382ba13df2a49d1..54095bd636257d352afec1a8a6a5d33970357f1f 100644 --- a/core/modules/search/tests/src/Functional/SearchNodeUpdateAndDeletionTest.php +++ b/core/modules/search/tests/src/Functional/SearchNodeUpdateAndDeletionTest.php @@ -3,6 +3,7 @@ namespace Drupal\Tests\search\Functional; use Drupal\Core\Database\Database; +use Drupal\search\SearchIndexInterface; use Drupal\Tests\BrowserTestBase; /** @@ -48,7 +49,8 @@ public function testSearchIndexUpdateOnNodeChange() { $node_search_plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); // Update the search index. $node_search_plugin->updateIndex(); - search_update_totals(); + $search_index = \Drupal::service('search.index'); + assert($search_index instanceof SearchIndexInterface); // Search the node to verify it appears in search results $edit = ['keys' => 'knights']; @@ -61,7 +63,6 @@ public function testSearchIndexUpdateOnNodeChange() { // Run indexer again $node_search_plugin->updateIndex(); - search_update_totals(); // Search again to verify the new text appears in test results. $edit = ['keys' => 'shrubbery']; @@ -83,7 +84,6 @@ public function testSearchIndexUpdateOnNodeDeletion() { $node_search_plugin = $this->container->get('plugin.manager.search')->createInstance('node_search'); // Update the search index. $node_search_plugin->updateIndex(); - search_update_totals(); // Search the node to verify it appears in search results $edit = ['keys' => 'dragons']; diff --git a/core/modules/search/tests/src/Functional/SearchPageCacheTagsTest.php b/core/modules/search/tests/src/Functional/SearchPageCacheTagsTest.php index 2b36a33c4fa8659817495f0807903685e75c61ea..320833e51fa66989dd3b35dc1c6a721a372300bb 100644 --- a/core/modules/search/tests/src/Functional/SearchPageCacheTagsTest.php +++ b/core/modules/search/tests/src/Functional/SearchPageCacheTagsTest.php @@ -55,7 +55,6 @@ protected function setUp() { $this->node->setOwner($this->searchingUser); $this->node->save(); $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); - search_update_totals(); } /** @@ -174,7 +173,6 @@ public function testSearchTagsBubbling() { // Refresh the search index. $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); - search_update_totals(); // Log in with searching user again. $this->drupalLogin($this->searchingUser); diff --git a/core/modules/search/tests/src/Functional/SearchPreprocessLangcodeTest.php b/core/modules/search/tests/src/Functional/SearchPreprocessLangcodeTest.php index 459c1baccfd0eb097ef31d7d394176b574fb6c69..9f0b991484e05a1e398e7183e15fe489e5df45f6 100644 --- a/core/modules/search/tests/src/Functional/SearchPreprocessLangcodeTest.php +++ b/core/modules/search/tests/src/Functional/SearchPreprocessLangcodeTest.php @@ -47,11 +47,6 @@ public function testPreprocessLangcode() { // First update the index. This does the initial processing. $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 - // function manually is needed to finish the indexing process. - search_update_totals(); - // Search for the additional text that is added by the preprocess // function. If you search for text that is in the node, preprocess is // not invoked on the node during the search excerpt generation. @@ -76,11 +71,6 @@ public function testPreprocessStemming() { // First update the index. This does the initial processing. $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 - // function manually is needed to finish the indexing process. - search_update_totals(); - // Search for the title of the node with a POST query. $edit = ['or' => 'testing']; $this->drupalPostForm('search/node', $edit, 'edit-submit--2'); diff --git a/core/modules/search/tests/src/Functional/SearchQueryAlterTest.php b/core/modules/search/tests/src/Functional/SearchQueryAlterTest.php index 12cb1d7d866bc83d02f5911dd757387cb1d628c1..e28d63d4aff8aae91a9670c7eae1eb7796ed28e1 100644 --- a/core/modules/search/tests/src/Functional/SearchQueryAlterTest.php +++ b/core/modules/search/tests/src/Functional/SearchQueryAlterTest.php @@ -41,7 +41,6 @@ public function testQueryAlter() { // Update the search index. $this->container->get('plugin.manager.search')->createInstance('node_search')->updateIndex(); - search_update_totals(); // Search for the body keyword 'pizza'. $this->drupalPostForm('search/node', ['keys' => 'pizza'], t('Search')); diff --git a/core/modules/search/tests/src/Functional/SearchRankingTest.php b/core/modules/search/tests/src/Functional/SearchRankingTest.php index f793232fc3df122c0bbbe8a652f779c3f21fa373..86c5ff656807fe913c59cd3d0d9aa1960be6a76f 100644 --- a/core/modules/search/tests/src/Functional/SearchRankingTest.php +++ b/core/modules/search/tests/src/Functional/SearchRankingTest.php @@ -9,6 +9,7 @@ use Drupal\Core\Url; use Drupal\filter\Entity\FilterFormat; use Drupal\search\Entity\SearchPage; +use Drupal\search\SearchIndexInterface; use Drupal\Tests\BrowserTestBase; use Drupal\Tests\Traits\Core\CronRunTrait; @@ -239,7 +240,8 @@ public function testHTMLRankings() { // Update the search index. $this->nodeSearch->getPlugin()->updateIndex(); - search_update_totals(); + $search_index = \Drupal::service('search.index'); + assert($search_index instanceof SearchIndexInterface); $this->nodeSearch->getPlugin()->setSearch('rocks', [], []); // Do the search and assert the results. @@ -264,7 +266,6 @@ public function testHTMLRankings() { // Update the search index. $this->nodeSearch->getPlugin()->updateIndex(); - search_update_totals(); $this->nodeSearch->getPlugin()->setSearch('rocks', [], []); // Do the search and assert the results. diff --git a/core/modules/search/tests/src/Functional/SearchSetLocaleTest.php b/core/modules/search/tests/src/Functional/SearchSetLocaleTest.php index bca3421169fdb4767ce9a44936ff5b0c373c99c9..5dee65879f12172f3594a01d423e880cf42b250b 100644 --- a/core/modules/search/tests/src/Functional/SearchSetLocaleTest.php +++ b/core/modules/search/tests/src/Functional/SearchSetLocaleTest.php @@ -34,7 +34,6 @@ protected function setUp() { $this->drupalCreateNode(['body' => [['value' => 'tapir']]]); // Update the search index. $this->nodeSearchPlugin->updateIndex(); - search_update_totals(); } /** diff --git a/core/modules/search/tests/src/Kernel/SearchDeprecationTest.php b/core/modules/search/tests/src/Kernel/SearchDeprecationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..a34633c39a54360d1e756b069b28e81502cb061f --- /dev/null +++ b/core/modules/search/tests/src/Kernel/SearchDeprecationTest.php @@ -0,0 +1,70 @@ +<?php + +namespace Drupal\Tests\search\Kernel; + +use Drupal\Core\Language\LanguageInterface; +use Drupal\KernelTests\KernelTestBase; + +/** + * Tests deprecated search methods. + * + * @group legacy + * @group search + */ +class SearchDeprecationTest extends KernelTestBase { + + /** + * {@inheritdoc} + */ + public static $modules = ['search']; + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->installSchema('search', [ + 'search_index', + 'search_dataset', + 'search_total', + ]); + $this->installConfig(['search']); + } + + /** + * @expectedDeprecation search_index() is deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use \Drupal\search\SearchIndex::index() instead. See https://www.drupal.org/node/3075696 + */ + public function testIndex() { + $this->assertNull(search_index('_test_', 1, LanguageInterface::LANGCODE_NOT_SPECIFIED, "foo")); + } + + /** + * @expectedDeprecation search_index_clear() is deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use \Drupal\search\SearchIndex::clear() instead. See https://www.drupal.org/node/3075696 + */ + public function testClear() { + $this->assertNull(search_index_clear()); + } + + /** + * @expectedDeprecation search_dirty() is deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use custom implementation of \Drupal\search\SearchIndexInterface instead. See https://www.drupal.org/node/3075696 + */ + public function testDirty() { + $this->assertNull(search_dirty("foo")); + $this->assertEqual([], search_dirty()); + } + + /** + * @expectedDeprecation search_update_totals() is deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use custom implementation of \Drupal\search\SearchIndexInterface instead. See https://www.drupal.org/node/3075696 + */ + public function testUpdateTotals() { + $this->assertNull(search_update_totals()); + } + + /** + * @expectedDeprecation search_mark_for_reindex() is deprecated in drupal:8.8.0 and is removed in drupal:9.0.0. Use \Drupal\search\SearchIndex::markForReindex() instead. See https://www.drupal.org/node/3075696 + */ + public function testMarkForReindex() { + $this->assertNull(search_mark_for_reindex('_test_', 1, LanguageInterface::LANGCODE_NOT_SPECIFIED)); + } + +} diff --git a/core/modules/search/tests/src/Kernel/SearchMatchTest.php b/core/modules/search/tests/src/Kernel/SearchMatchTest.php index 577028bd48eec21064d57c083245bb5a48f12447..f0de1ec8c018eb1006e2531603b64c40106391de 100644 --- a/core/modules/search/tests/src/Kernel/SearchMatchTest.php +++ b/core/modules/search/tests/src/Kernel/SearchMatchTest.php @@ -5,6 +5,7 @@ use Drupal\Core\Database\Database; use Drupal\Core\Language\LanguageInterface; use Drupal\KernelTests\KernelTestBase; +use Drupal\search\SearchIndexInterface; /** * Indexes content and queries it. @@ -49,11 +50,13 @@ public function testMatching() { public function _setup() { $this->config('search.settings')->set('index.minimum_word_size', 3)->save(); + $search_index = \Drupal::service('search.index'); + assert($search_index instanceof SearchIndexInterface); for ($i = 1; $i <= 7; ++$i) { - search_index(static::SEARCH_TYPE, $i, LanguageInterface::LANGCODE_NOT_SPECIFIED, $this->getText($i)); + $search_index->index(static::SEARCH_TYPE, $i, LanguageInterface::LANGCODE_NOT_SPECIFIED, $this->getText($i)); } for ($i = 1; $i <= 5; ++$i) { - search_index(static::SEARCH_TYPE_2, $i + 7, LanguageInterface::LANGCODE_NOT_SPECIFIED, $this->getText2($i)); + $search_index->index(static::SEARCH_TYPE_2, $i + 7, LanguageInterface::LANGCODE_NOT_SPECIFIED, $this->getText2($i)); } // No getText builder function for Japanese text; just a simple array. foreach ([ @@ -61,9 +64,8 @@ public function _setup() { 14 => 'ドルーパルが大好きよ!', 15 => 'コーヒーとケーキ', ] as $i => $jpn) { - search_index(static::SEARCH_TYPE_JPN, $i, LanguageInterface::LANGCODE_NOT_SPECIFIED, $jpn); + $search_index->index(static::SEARCH_TYPE_JPN, $i, LanguageInterface::LANGCODE_NOT_SPECIFIED, $jpn); } - search_update_totals(); } /**