You are here

search_api_solr.module in Search API Solr 8.2

File

search_api_solr.module
View source
<?php

use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Messenger\MessengerInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\search_api\Entity\Server;
use Drupal\search_api\SearchApiException;
use Drupal\search_api\ServerInterface;
use Drupal\search_api_solr\Plugin\search_api\backend\SearchApiSolrAnySchemaBackend;
use Drupal\search_api_solr\SolrBackendInterface;

/**
 * Implements hook_help().
 */
function search_api_solr_help($route_name, RouteMatchInterface $route_match) {
  if ($route_name == 'search_api.overview') {

    // Included because we need the REQUIREMENT_* constants.
    include_once DRUPAL_ROOT . '/core/includes/install.inc';
    module_load_include('install', 'search_api_solr');
    $reqs = search_api_solr_requirements('runtime');
    foreach ($reqs as $req) {
      if (isset($req['description'])) {
        $type = $req['severity'] == REQUIREMENT_ERROR ? MessengerInterface::TYPE_ERROR : ($req['severity'] == REQUIREMENT_WARNING ? MessengerInterface::TYPE_WARNING : MessengerInterface::TYPE_STATUS);
        \Drupal::messenger()
          ->addMessage($req['description'], $type);
      }
    }
  }
}

/**
 * Implements hook_cron().
 *
 * Used to execute an optimization operation on all enabled Solr servers once a
 * day.
 *
 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
 * @throws \Drupal\search_api\SearchApiException
 */
function search_api_solr_cron() {
  $request_time = \Drupal::time()
    ->getRequestTime();
  $action = \Drupal::config('search_api_solr.settings')
    ->get('cron_action');

  // We treat all unknown action settings as "none". However, we turn a blind
  // eye for Britons and other people who can spell.
  if (!in_array($action, [
    'spellcheck',
    'optimize',
    'optimise',
  ])) {
    return;
  }

  // 86400 seconds is one day. We use slightly less here to allow for some
  // variation in the request time of the cron run, so that the time of day will
  // (more or less) stay the same.
  if ($request_time - \Drupal::state()
    ->get('search_api_solr.last_optimize') > 86340) {
    \Drupal::state()
      ->set('search_api_solr.last_optimize', $request_time);
    foreach (search_api_solr_get_servers() as $server) {
      try {

        /** @var \Drupal\search_api_solr\SolrBackendInterface $backend */
        $backend = $server
          ->getBackend();
        $connector = $backend
          ->getSolrConnector();
        $indexes = $server
          ->getIndexes();
        if (!$indexes) {
          continue;
        }
        foreach ($indexes as $index) {
          $backend
            ->finalizeIndex($index);
          $backend
            ->getLogger()
            ->info('Finalized Solr server @server.', [
            '@server' => $server
              ->label(),
          ]);
        }
        if ($action != 'spellcheck') {
          $connector
            ->optimize();
          $backend
            ->getLogger()
            ->info('Optimized Solr server @server.', [
            '@server' => $server
              ->label(),
          ]);
        }
        else {
          $autocomplete_query = $connector
            ->getAutocompleteQuery();
          $spellcheck_component = $autocomplete_query
            ->getSpellcheck();
          $spellcheck_component
            ->setBuild(TRUE);

          // Terms don't need to be build. Suggesters are configured to be
          // buildOnCommit.
          $connector
            ->execute($autocomplete_query);
          $backend
            ->getLogger()
            ->info('Rebuilt spellcheck dictionary on Solr server @server.', [
            '@server' => $server
              ->label(),
          ]);
        }
      } catch (SearchApiException $e) {
        watchdog_exception('search_api', $e, '%type while optimizing Solr server @server: @message in %function (line %line of %file).', [
          '@server' => $server
            ->label(),
        ]);
      }
    }

    // Delete cached endpoint data once a day.
    \Drupal::state()
      ->delete('search_api_solr.endpoint.data');
  }
}

/**
 * Get the default third party settings of an index for Solr.
 *
 * @return array
 */
function search_api_solr_default_index_third_party_settings() {
  return [
    'finalize' => FALSE,
    'commit_before_finalize' => FALSE,
    'commit_after_finalize' => FALSE,
    'highlighter' => [
      'maxAnalyzedChars' => 51200,
      'fragmenter' => 'gap',
      'usePhraseHighlighter' => TRUE,
      'highlightMultiTerm' => TRUE,
      'preserveMulti' => FALSE,
      'regex' => [
        'slop' => 0.5,
        'pattern' => 'blank',
        'maxAnalyzedChars' => 10000,
      ],
      'highlight' => [
        'mergeContiguous' => FALSE,
        'requireFieldMatch' => FALSE,
        'snippets' => 3,
        'fragsize' => 0,
      ],
    ],
    'advanced' => [
      'index_prefix' => '',
    ],
  ];
}

/**
 * Implements hook_form_FORM_alter.
 */
function search_api_solr_form_search_api_index_form_alter(&$form, FormStateInterface $form_state, $form_id) {

  // We need to restrict by form ID here because this function is also called
  // via hook_form_BASE_FORM_ID_alter (which is wrong, e.g. in the case of the
  // form ID search_api_field_config).
  if (in_array($form_id, [
    'search_api_index_form',
    'search_api_index_edit_form',
  ])) {
    if (isset($form['server'])) {
      $form['server']['#element_validate'][] = 'search_api_solr_form_search_api_index_form_validate_server';
    }
    $settings = [];

    /** @var \Drupal\search_api\IndexInterface $index */
    $index = $form_state
      ->getFormObject()
      ->getEntity();
    if (!$index
      ->isNew()) {
      $settings = $index
        ->getThirdPartySettings('search_api_solr');
    }
    $settings += search_api_solr_default_index_third_party_settings();
    $form['third_party_settings']['search_api_solr'] = [
      '#tree' => TRUE,
      '#type' => 'details',
      '#title' => t('Solr specific index options'),
      '#collapsed' => TRUE,
    ];
    $form['third_party_settings']['search_api_solr']['finalize'] = [
      '#type' => 'checkbox',
      '#title' => t('Finalize index before first search'),
      '#description' => t('If enabled, other modules could hook in to apply "finalizations" to the index after updates or deletions happend to index items.'),
      '#default_value' => $settings['finalize'],
    ];
    $form['third_party_settings']['search_api_solr']['commit_before_finalize'] = [
      '#type' => 'checkbox',
      '#title' => t('Wait for commit before first finalization'),
      '#description' => t('If enabled, Solr will be be forced to flush all commits before any "finalizations" will be applied.'),
      '#default_value' => $settings['commit_before_finalize'],
      '#states' => [
        'invisible' => [
          ':input[name="third_party_settings[search_api_solr][finalize]"]' => [
            'checked' => FALSE,
          ],
        ],
      ],
    ];
    $form['third_party_settings']['search_api_solr']['commit_after_finalize'] = [
      '#type' => 'checkbox',
      '#title' => t('Wait for commit after last finalization'),
      '#description' => t('If enabled, Solr will be be forced to flush all commits after the last "finalizations" have been applied.'),
      '#default_value' => $settings['commit_after_finalize'],
      '#states' => [
        'invisible' => [
          ':input[name="third_party_settings[search_api_solr][finalize]"]' => [
            'checked' => FALSE,
          ],
        ],
      ],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter'] = [
      '#tree' => TRUE,
      '#type' => 'details',
      '#title' => t('Highlighter'),
      '#collapsed' => TRUE,
      '#description' => t('If "Retrieve result data from Solr" and "Highlight retrieved data" are selected for the Solr backend on the server edit page, these highlighting settings will be used.'),
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['maxAnalyzedChars'] = [
      '#type' => 'number',
      '#min' => 0,
      '#title' => t('maxAnalyzedChars'),
      '#description' => t('Specifies the number of characters into a document that Solr should look for suitable snippets.'),
      '#default_value' => $settings['highlighter']['maxAnalyzedChars'],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['fragmenter'] = [
      '#type' => 'select',
      '#options' => [
        'gap' => 'gap',
        'regex' => 'regex',
      ],
      '#title' => t('fragmenter'),
      '#description' => t('Specifies a text snippet generator for highlighted text. The standard fragmenter is gap, which creates fixed-sized fragments with gaps for multi-valued fields. Another option is regex, which tries to create fragments that resemble a specified regular expression. This parameter accepts per-field overrdes.'),
      '#default_value' => $settings['highlighter']['fragmenter'],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['regex'] = [
      '#tree' => TRUE,
      '#type' => 'details',
      '#title' => t('regex'),
      '#collapsed' => FALSE,
      '#states' => [
        'invisible' => [
          'select[name="third_party_settings[search_api_solr][highlighter][fragmenter]"]' => [
            'value' => 'gap',
          ],
        ],
      ],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['regex']['slop'] = [
      '#type' => 'number',
      '#step' => 0.1,
      '#min' => 0,
      '#title' => t('regex.slop'),
      '#description' => t('When using the regex fragmenter, this parameter defines the factor by which the fragmenter can stray from the ideal fragment size (given by fragsize) to accommodate a regular expression. For instance, a slop of 0.2 with fragsize=100 should yield fragments between 80 and 120 characters in length. It is usually good to provide a slightly smaller fragsize value when using the regex fragmenter.'),
      '#default_value' => $settings['highlighter']['regex']['slop'],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['regex']['pattern'] = [
      '#type' => 'textfield',
      '#title' => t('regex.pattern'),
      '#description' => t('Specifies the regular expression for fragmenting. This could be used to extract sentences.'),
      '#default_value' => $settings['highlighter']['regex']['pattern'],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['regex']['maxAnalyzedChars'] = [
      '#type' => 'number',
      '#min' => 0,
      '#title' => t('regex.maxAnalyzedChars'),
      '#description' => t('Instructs Solr to analyze only this many characters from a field when using the regex fragmenter (after which, the fragmenter produces fixed-sized fragments). Applying a complicated regex to a huge field is computationally expensive.'),
      '#default_value' => $settings['highlighter']['regex']['maxAnalyzedChars'],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['usePhraseHighlighter'] = [
      '#type' => 'checkbox',
      '#title' => t('usePhraseHighlighter'),
      '#description' => t('If set, Solr will highlight phrase queries (and other advanced position-sensitive queries) accurately. If false, the parts of the phrase will be highlighted everywhere instead of only when it forms the given phrase.'),
      '#default_value' => $settings['highlighter']['usePhraseHighlighter'],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['highlightMultiTerm'] = [
      '#type' => 'checkbox',
      '#title' => t('highlightMultiTerm'),
      '#description' => t('If set, Solr will highlight wildcard queries (and other MultiTermQuery subclasses). If false, they won\'t be highlighted at all.'),
      '#default_value' => $settings['highlighter']['highlightMultiTerm'],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['preserveMulti'] = [
      '#type' => 'checkbox',
      '#title' => t('preserveMulti'),
      '#description' => t('If set, multi-valued fields will return all values in the order they were saved in the index. If false, only values that match the highlight request will be returned.'),
      '#default_value' => $settings['highlighter']['preserveMulti'],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['highlight']['mergeContiguous'] = [
      '#type' => 'checkbox',
      '#title' => t('mergeContiguous'),
      '#description' => t('Instructs Solr to collapse contiguous fragments into a single fragment. A value of true indicates contiguous fragments will be collapsed into single fragment. This parameter accepts per-field overrides. The default value, false, is also the backward-compatible setting.'),
      '#default_value' => $settings['highlighter']['highlight']['mergeContiguous'],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['highlight']['requireFieldMatch'] = [
      '#type' => 'checkbox',
      '#title' => t('requireFieldMatch'),
      '#description' => t('If set, highlights terms only if they appear in the specified field. If not set, terms are highlighted in all requested fields regardless of which field matched the query.'),
      '#default_value' => $settings['highlighter']['highlight']['requireFieldMatch'],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['highlight']['snippets'] = [
      '#type' => 'number',
      '#min' => 0,
      '#title' => t('snippets'),
      '#description' => t('Specifies maximum number of highlighted snippets to generate per field. It is possible for any number of snippets from zero to this value to be generated. This parameter accepts per-field overrides.'),
      '#default_value' => $settings['highlighter']['highlight']['snippets'],
    ];
    $form['third_party_settings']['search_api_solr']['highlighter']['highlight']['fragsize'] = [
      '#type' => 'number',
      '#min' => 0,
      '#title' => t('fragsize'),
      '#description' => t('Specifies the size, in characters, of fragments to consider for highlighting. 0 indicates that no fragmenting should be considered and the whole field value should be used. This parameter accepts per-field overrides.'),
      '#default_value' => $settings['highlighter']['highlight']['fragsize'],
    ];
    $form['third_party_settings']['search_api_solr']['advanced'] = [
      '#tree' => TRUE,
      '#type' => 'details',
      '#title' => t('Advanced'),
    ];
    $form['third_party_settings']['search_api_solr']['advanced']['index_prefix'] = [
      '#type' => 'textfield',
      '#title' => t('Index prefix'),
      '#description' => t("By default, the index ID in the Solr server is the same as the index's machine name in Drupal. This setting will let you specify an additional prefix. Only use alphanumeric characters and underscores. Since changing the prefix makes the currently indexed data inaccessible, you should not change this variable when no data is indexed."),
      '#default_value' => $settings['advanced']['index_prefix'],
    ];
  }
}

/**
 *
 */
function search_api_solr_form_search_api_index_form_validate_server(&$element, FormStateInterface $form_state, $form) {
  if ($server = Server::load($form_state
    ->getValue('server'))) {
    if ($server
      ->getBackend() instanceof SolrBackendInterface) {

      /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
      $form_object = $form_state
        ->getFormObject();
      $this_index = $form_object
        ->getEntity();
      $indexes = $server
        ->getIndexes();
      $index_count = 0;
      foreach ($indexes as $index) {
        if ($index
          ->status()) {
          if (!$this_index
            ->isNew() && $this_index
            ->id() == $index
            ->id()) {
            continue;
          }
          ++$index_count;
        }
      }
      if ($index_count > 0 && $form_state
        ->getValue('status')) {
        \Drupal::messenger()
          ->addWarning(t("You're storing multiple indexes on the same Solr index (aka core). Take care if you use advanced Solr features like spell checking, suggesters, terms, autocomplete and others directly, because they aren't aware of these multiple indexes by default. Use Search API family modules like Autocomplete module instead which will help you to avoid issues."));
      }
    }
  }
}

/**
 * Implements hook_search_api_views_handler_mapping_alter()
 *
 * @param array $mapping
 *   An associative array with data types as the keys and Views field data
 *   definitions as the values. In addition to all normally defined data types,
 *   keys can also be "options" for any field with an options list, "entity" for
 *   general entity-typed fields or "entity:ENTITY_TYPE" (with "ENTITY_TYPE"
 *   being the machine name of an entity type) for entities of that type.
 *
 * @see _search_api_views_handler_mapping()
 */
function search_api_solr_search_api_views_handler_mapping_alter(&$mapping) {
  $mapping['solr_text_ngram'] = $mapping['solr_text_omit_norms'] = $mapping['solr_text_phonetic'] = $mapping['solr_text_suggester'] = $mapping['solr_text_unstemmed'] = $mapping['solr_text_wstoken'] = [
    'argument' => [
      'id' => 'search_api',
    ],
    'filter' => [
      'id' => 'search_api_fulltext',
    ],
    'sort' => [
      'id' => 'search_api',
    ],
  ];
  $mapping['solr_string_ngram'] = [
    'argument' => [
      'id' => 'search_api',
    ],
    'filter' => [
      'id' => 'search_api_string',
    ],
    'sort' => [
      'id' => 'search_api',
    ],
  ];
}

/**
 * Implements hook_views_data_alter().
 *
 * Remove fields from solr_document datasources from the views data. Datasource
 * fields that have been added to the index would be duplicated in the Views Add
 * fields list. Fields that aren't added to the index can't be displayed.
 */
function search_api_solr_views_data_alter(array &$data) {

  // @todo check for a search_api based view first.
  foreach ($data as $key => $fields) {
    if (preg_match('/search_api_datasource_(.+)_solr_document/', $key)) {
      unset($data[$key]);
    }
  }
}

/**
 * Deletes all Solr Field Type and re-installs them from their yml files.
 */
function search_api_solr_delete_and_reinstall_all_field_types() {
  $storage = \Drupal::entityTypeManager()
    ->getStorage('solr_field_type');
  $storage
    ->delete($storage
    ->loadMultiple());

  /** @var \Drupal\Core\Config\ConfigInstallerInterface $config_installer */
  $config_installer = \Drupal::service('config.installer');
  $config_installer
    ->installDefaultConfig('module', 'search_api_solr');
  $restrict_by_dependency = [
    'module' => 'search_api_solr',
  ];
  $config_installer
    ->installOptionalConfig(NULL, $restrict_by_dependency);
}

/**
 * Get all Search API servers that use a Solr backend.
 *
 * @param bool $only_active
 *
 * @return ServerInterface[]
 *
 * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
 * @throws \Drupal\search_api\SearchApiException
 */
function search_api_solr_get_servers($only_active = TRUE) {
  $solr_servers = [];
  $storage = \Drupal::entityTypeManager()
    ->getStorage('search_api_server');

  /** @var ServerInterface[] $servers */
  $servers = $only_active ? $storage
    ->loadByProperties([
    'status' => TRUE,
  ]) : $storage
    ->loadMultiple();
  foreach ($servers as $server) {
    if ($server
      ->hasValidBackend() && $server
      ->getBackend() instanceof SolrBackendInterface) {
      $solr_servers[] = $server;
    }
  }
  return $solr_servers;
}

/**
 * Implements hook_entity_operation().
 *
 * Adds an operation to Solr servers to directly generate and download a config.
 *
 * @param \Drupal\Core\Entity\EntityInterface $server
 *
 * @return array
 * @throws \Drupal\search_api\SearchApiException
 */
function search_api_solr_entity_operation(\Drupal\Core\Entity\EntityInterface $server) {
  $operations = [];
  if ($server instanceof Server && $server
    ->getBackend() instanceof SolrBackendInterface && !$server
    ->getBackend() instanceof SearchApiSolrAnySchemaBackend) {
    $operations['get_config_zip'] = [
      'title' => t('Get config.zip'),
      'url' => \Drupal\Core\Url::fromRoute('entity.solr_field_type.config_zip_collection', [
        'search_api_server' => $server
          ->id(),
      ]),
      'weight' => 50,
    ];
  }
  return $operations;
}

/**
 * Implements function hook_search_api_items_indexed().
 *
 * @param \Drupal\search_api\Entity\Index $index
 * @param string[] $processed_ids
 *
 * @throws \Drupal\search_api\SearchApiException
 */
function search_api_solr_search_api_items_indexed($index, $processed_ids) {
  if ($index
    ->getServerInstance()
    ->getBackend() instanceof SolrBackendInterface) {

    // The _search_all() streaming expression needs a row limit that much higher
    // then the real number of rows. Therefore we set a 10 times higher row
    // limit as indexed documents. To increase the number of query result cache
    // hits we set "normalize" the nearest higher power of 2.
    $rows = $index
      ->getTrackerInstance()
      ->getIndexedItemsCount() * 10;
    for ($i = pow(2, 14); $i < $rows; $i *= 2) {
    }
    \Drupal::state()
      ->set('search_api_solr.' . $index
      ->id() . '.search_all_rows', $i);
  }
}