You are here

public function SearchApiDbService::fieldsUpdated in Search API Database Search 7

Overrides SearchApiAbstractService::fieldsUpdated().

Internally, this is also used by addIndex().

Overrides SearchApiAbstractService::fieldsUpdated

2 calls to SearchApiDbService::fieldsUpdated()
SearchApiDbService::addIndex in ./service.inc
Implements SearchApiServiceInterface::__construct().
SearchApiDbService::indexItem in ./service.inc
Indexes a single item on the specified index.

File

./service.inc, line 506
Contains SearchApiDbService.

Class

SearchApiDbService
Indexes and searches items using the database.

Code

public function fieldsUpdated(SearchApiIndex $index) {
  try {
    $fields =& $this->options['indexes'][$index->machine_name];
    $new_fields = $index
      ->getFields();
    $reindex = FALSE;
    $cleared = FALSE;
    $change = FALSE;
    $text_table = NULL;
    $missing_text_tables = array();
    foreach ($fields as $name => $field) {
      if (!isset($text_table) && search_api_is_text_type($field['type'])) {

        // Stash the shared text table name for the index, if it exists.
        // Otherwise, there was some error previously and we have to remember
        // to later come back and set the correct table here.
        if ($this->connection
          ->schema()
          ->tableExists($field['table'])) {
          $text_table = $field['table'];
        }
        else {
          $missing_text_tables[$name] = $name;
        }
      }
      if (!isset($new_fields[$name])) {

        // The field is no longer in the index, drop the data.
        $this
          ->removeFieldStorage($name, $field);
        unset($fields[$name]);
        $change = TRUE;
        continue;
      }
      $old_type = $field['type'];
      $new_type = $new_fields[$name]['type'];
      $fields[$name]['type'] = $new_type;
      $fields[$name]['boost'] = $new_fields[$name]['boost'];
      $old_inner_type = search_api_extract_inner_type($old_type);
      $new_inner_type = search_api_extract_inner_type($new_type);
      if ($old_type != $new_type) {
        $change = TRUE;
        $list_old = (bool) search_api_list_nesting_level($old_type);
        $list_new = (bool) search_api_list_nesting_level($new_type);
        if ($old_inner_type == 'text' || $new_inner_type == 'text' || $list_old != $list_new) {

          // A change in fulltext or list status necessitates completely
          // clearing the index.
          $reindex = TRUE;
          if (!$cleared) {
            $cleared = TRUE;
            $this
              ->deleteItems('all', $index);
          }
          $this
            ->removeFieldStorage($name, $field);

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

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

          // Even though the SQL type stays the same, we have to reindex since
          // conversion rules change.
          $reindex = TRUE;
        }
      }
      elseif ($text_table && $new_inner_type == 'text' && $field['boost'] != $new_fields[$name]['boost']) {
        $change = TRUE;
        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[$name]['boost'] / $field['boost'];
            $this->connection
              ->update($text_table)
              ->expression('score', 'score * :mult', array(
              ':mult' => $multiplier,
            ))
              ->condition('field_name', self::getTextFieldName($name))
              ->execute();
          }
          else {
            $reindex = TRUE;
          }
        }
      }

      // Make sure the table and column now exist. (Especially important when
      // we actually add the index for the first time.)
      if (!search_api_is_text_type($field['type'])) {
        $storageExists = $this->connection
          ->schema()
          ->tableExists($field['table']) && (!isset($field['column']) || $this->connection
          ->schema()
          ->fieldExists($field['table'], $field['column']));
        if (!$storageExists) {
          $this
            ->createFieldTable($index, $new_fields[$name], $field);
        }
      }
      elseif ($text_table && $fields[$name]['table'] != $text_table) {
        $fields[$name]['table'] = $text_table;
        $change = TRUE;
      }
      unset($new_fields[$name]);
    }
    $prefix = 'search_api_db_' . $index->machine_name;

    // These are new fields that were previously not indexed.
    foreach ($new_fields as $name => $field) {
      $reindex = TRUE;
      if (search_api_is_text_type($field['type'])) {
        if (!isset($text_table)) {

          // If we have not encountered a text table, assign a name for it.
          $text_table = $this
            ->findFreeTable($prefix . '_', 'text');
        }
        $fields[$name] = array(
          'table' => $text_table,
        );
      }
      else {
        if ($this
          ->canDenormalize($field)) {
          $fields[$name] = array(
            'table' => $prefix,
            'column' => $this
              ->findFreeColumn($prefix, $name),
          );
        }
        else {
          $fields[$name] = array(
            'table' => $this
              ->findFreeTable($prefix . '_', $name),
          );
        }
        $this
          ->createFieldTable($index, $field, $fields[$name]);
      }
      $fields[$name]['type'] = $field['type'];
      $fields[$name]['boost'] = $field['boost'];
      $change = TRUE;
    }

    // If there were fulltext fields without valid table set, set it now.
    if ($missing_text_tables) {
      if (!isset($text_table)) {

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

    // If needed, make sure the text table exists.
    if (isset($text_table) && !$this->connection
      ->schema()
      ->tableExists($text_table)) {
      $table = array(
        'name' => $text_table,
        'module' => 'search_api_db',
        'fields' => array(
          'item_id' => array(
            'description' => 'The primary identifier of the item.',
            'not null' => TRUE,
          ),
          'field_name' => array(
            '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' => 255,
          ),
          'word' => array(
            'description' => 'The text of the indexed token.',
            'type' => 'varchar',
            'length' => 50,
            'not null' => TRUE,
          ),
          'score' => array(
            'description' => 'The score associated with this token.',
            'type' => 'int',
            'unsigned' => TRUE,
            'not null' => TRUE,
            'default' => 0,
          ),
        ),
        'indexes' => array(
          'word_field' => array(
            array(
              'word',
              20,
            ),
            'field_name',
          ),
        ),
        // Add a covering index since word is not repeated for each item.
        'primary key' => array(
          'item_id',
          'field_name',
          'word',
        ),
      );

      // The type of the item_id field depends on the ID field's type.
      $id_field = $index
        ->datasource()
        ->getIdFieldInfo();
      $table['fields']['item_id'] += $this
        ->sqlType($id_field['type'] == 'text' ? 'string' : $id_field['type']);
      if (isset($table['fields']['item_id']['length'])) {

        // A length of 255 is overkill for IDs. 50 should be more than enough.
        $table['fields']['item_id']['length'] = 50;
      }
      $this->connection
        ->schema()
        ->createTable($text_table, $table);

      // Some DBMSs will need a character encoding and collation set. Since
      // this largely circumvents Drupal's database layer (but isn't integral
      // enough to fail completely when it doesn't work), we wrap it in a
      // try/catch, to be on the safe side.
      try {
        switch ($this->connection
          ->databaseType()) {
          case 'mysql':
            $this->connection
              ->query("ALTER TABLE {{$text_table}} CONVERT TO CHARACTER SET 'utf8' COLLATE 'utf8_bin'");
            break;
          case 'pgsql':
            $this->connection
              ->query("ALTER TABLE {{$text_table}} ALTER COLUMN word SET DATA TYPE character varying(50) COLLATE \"C\"");
            break;

          // @todo Add fixes for other DBMSs.
          case 'oracle':
          case 'sqlite':
          case 'sqlsrv':
            break;
        }
      } catch (PDOException $e) {
        $vars['%index'] = $index->name;
        watchdog_exception('search_api_db', $e, '%type while trying to change collation for the fulltext table of index %index: !message in %function (line %line of %file).', $vars);
      }
    }
    if ($change) {
      $this->server
        ->save();
    }
    return $reindex;
  } catch (Exception $e) {
    throw new SearchApiException($e
      ->getMessage());
  }
}