You are here

protected function Database::fieldsUpdated in Search API 8

Updates the storage tables when the field configuration changes.

Parameters

\Drupal\search_api\IndexInterface $index: The search index whose fields (might) have changed.

Return value

bool TRUE if the data needs to be reindexed, FALSE otherwise.

Throws

\Drupal\search_api\SearchApiException Thrown if any exceptions occur internally – for example, in the database layer.

3 calls to Database::fieldsUpdated()
Database::addIndex in modules/search_api_db/src/Plugin/search_api/backend/Database.php
Adds a new index to this server.
Database::indexItem in modules/search_api_db/src/Plugin/search_api/backend/Database.php
Indexes a single item on the specified index.
Database::updateIndex in modules/search_api_db/src/Plugin/search_api/backend/Database.php
Notifies the server that an index attached to it has been changed.

File

modules/search_api_db/src/Plugin/search_api/backend/Database.php, line 933

Class

Database
Indexes and searches items using the database.

Namespace

Drupal\search_api_db\Plugin\search_api\backend

Code

protected function fieldsUpdated(IndexInterface $index) {
  try {
    $db_info = $this
      ->getIndexDbInfo($index);
    $fields =& $db_info['field_tables'];
    $new_fields = $index
      ->getFields();
    $new_fields += $this
      ->getSpecialFields($index);
    $reindex = FALSE;
    $cleared = FALSE;
    $text_table = NULL;
    $denormalized_table = $db_info['index_table'];
    foreach ($fields as $field_id => $field) {
      $was_text_type = $this
        ->getDataTypeHelper()
        ->isTextType($field['type']);
      if (!isset($text_table) && $was_text_type) {

        // Stash the shared text table name for the index.
        $text_table = $field['table'];
      }
      if (!isset($new_fields[$field_id])) {

        // The field is no longer in the index, drop the data.
        $this
          ->removeFieldStorage($field_id, $field, $denormalized_table);
        unset($fields[$field_id]);
        continue;
      }
      $old_type = $field['type'];
      $new_type = $new_fields[$field_id]
        ->getType();
      $fields[$field_id]['type'] = $new_type;
      $fields[$field_id]['boost'] = $new_fields[$field_id]
        ->getBoost();
      if ($old_type != $new_type) {
        $is_text_type = $this
          ->getDataTypeHelper()
          ->isTextType($new_type);
        if ($was_text_type || $is_text_type) {

          // A change in fulltext status necessitates completely clearing the
          // index.
          $reindex = TRUE;
          if (!$cleared) {
            $cleared = TRUE;
            $this
              ->deleteAllIndexItems($index);
          }
          $this
            ->removeFieldStorage($field_id, $field, $denormalized_table);

          // Keep the table in $new_fields to create the new storage.
          continue;
        }
        elseif ($this
          ->sqlType($old_type) != $this
          ->sqlType($new_type)) {

          // There is a change in SQL type. We don't have to clear the index,
          // since types can be converted.
          $sql_spec = $this
            ->sqlType($new_type);
          $sql_spec += [
            'description' => "The field's value for this item",
          ];
          $this->database
            ->schema()
            ->changeField($denormalized_table, $field['column'], $field['column'], $sql_spec);
          $sql_spec['not null'] = TRUE;
          $this->database
            ->schema()
            ->changeField($field['table'], 'value', 'value', $sql_spec);
          $reindex = TRUE;
        }
        elseif ($old_type == 'date' || $new_type == 'date') {

          // Even though the SQL type stays the same, we have to reindex since
          // conversion rules change.
          $reindex = TRUE;
        }
      }
      elseif ($was_text_type && $field['boost'] != $new_fields[$field_id]
        ->getBoost()) {
        if (!$reindex) {

          // If there was a non-zero boost set previously, we can just update
          // all scores with a single UPDATE query. Otherwise, no way around
          // re-indexing.
          if ($field['boost']) {
            $multiplier = $new_fields[$field_id]
              ->getBoost() / $field['boost'];

            // Postgres doesn't allow multiplying an integer column with a
            // float literal, so we have to work around that.
            $expression = 'score * :mult';
            $args = [
              ':mult' => $multiplier,
            ];
            if (is_float($multiplier) && ($pos = strpos("{$multiplier}", '.'))) {
              $expression .= ' / :div';
              $after_point_digits = strlen("{$multiplier}") - $pos - 1;
              $args[':div'] = pow(10, min(3, $after_point_digits));
              $args[':mult'] = (int) round($args[':mult'] * $args[':div']);
            }
            $this->database
              ->update($text_table)
              ->expression('score', $expression, $args)
              ->condition('field_name', static::getTextFieldName($field_id))
              ->execute();
          }
          else {
            $reindex = TRUE;
          }
        }
      }

      // Make sure the table and column now exist. (Especially important when
      // we actually add the index for the first time.)
      $storage_exists = empty($field['table']) || $this->database
        ->schema()
        ->fieldExists($field['table'], 'value');
      $denormalized_storage_exists = $this->database
        ->schema()
        ->fieldExists($denormalized_table, $field['column']);
      if (!$was_text_type && !$storage_exists) {
        $db = [
          'table' => $field['table'],
        ];
        $this
          ->createFieldTable($new_fields[$field_id], $db);
      }

      // Ensure that a column is created in the denormalized storage even for
      // 'text' fields.
      if (!$denormalized_storage_exists) {
        $db = [
          'table' => $denormalized_table,
          'column' => $field['column'],
        ];
        $this
          ->createFieldTable($new_fields[$field_id], $db, 'index');
      }
      unset($new_fields[$field_id]);
    }
    $prefix = 'search_api_db_' . $index
      ->id();

    // These are new fields that were previously not indexed.
    foreach ($new_fields as $field_id => $field) {
      $reindex = TRUE;
      $fields[$field_id] = [];
      if ($this
        ->getDataTypeHelper()
        ->isTextType($field
        ->getType())) {
        if (!isset($text_table)) {

          // If we have not encountered a text table, assign a name for it.
          $text_table = $this
            ->findFreeTable($prefix . '_', 'text');
        }
        $fields[$field_id]['table'] = $text_table;
      }
      else {
        $fields[$field_id]['table'] = $this
          ->findFreeTable($prefix . '_', $field_id);
        $this
          ->createFieldTable($field, $fields[$field_id]);
      }

      // Always add a column in the denormalized table.
      $fields[$field_id]['column'] = $this
        ->findFreeColumn($denormalized_table, $field_id);
      $this
        ->createFieldTable($field, [
        'table' => $denormalized_table,
        'column' => $fields[$field_id]['column'],
      ], 'index');
      $fields[$field_id]['type'] = $field
        ->getType();
      $fields[$field_id]['boost'] = $field
        ->getBoost();
    }

    // If needed, make sure the text table exists.
    if (isset($text_table) && !$this->database
      ->schema()
      ->tableExists($text_table)) {
      $table = [
        'name' => $text_table,
        'module' => 'search_api_db',
        'fields' => [
          'item_id' => [
            'type' => 'varchar',
            'length' => 150,
            'description' => 'The primary identifier of the item',
            'not null' => TRUE,
          ],
          'field_name' => [
            'description' => "The name of the field in which the token appears, or a base-64 encoded sha-256 hash of the field",
            'not null' => TRUE,
            'type' => 'varchar',
            'length' => 191,
          ],
          'word' => [
            'description' => 'The text of the indexed token',
            'type' => 'varchar',
            'length' => 50,
            'not null' => TRUE,
            'binary' => TRUE,
          ],
          'score' => [
            'description' => 'The score associated with this token',
            'type' => 'int',
            'unsigned' => TRUE,
            'not null' => TRUE,
            'default' => 0,
          ],
        ],
        'indexes' => [
          'word_field' => [
            [
              'word',
              20,
            ],
            'field_name',
          ],
        ],
        // Add a covering index since word is not repeated for each item.
        'primary key' => [
          'item_id',
          'field_name',
          'word',
        ],
      ];
      $this->database
        ->schema()
        ->createTable($text_table, $table);
      $this->dbmsCompatibility
        ->alterNewTable($text_table, 'text');
    }
    $this
      ->getKeyValueStore()
      ->set($index
      ->id(), $db_info);
    return $reindex;
  } catch (\Exception $e) {
    throw new SearchApiException($e
      ->getMessage(), $e
      ->getCode(), $e);
  }
}