Drupal 8: Creating Custom Fields In Search API

Pushing data from Drupal into Solr is a really convenient way of creating a robust and extensible search solution. The Search API module has a number of different fields available that can be used to integrate with all sorts of fields, but what isn't included is computed fields or other data.

Thankfully, adding a custom field into the Search API system doesn't need a lot of code. A single class, with an optional hook, is all that's needed to het everything working.

I was recently looking at the node view count module that was being used to record what users viewed what nodes on a Drupal site. What was needed was a report page that had a bunch of data from different fields of a node, along with the node view count data. As this data wasn't immediately available to Solr I needed to find a way to inject the data into Solr using the mechanisms that Search API has. 

To create a Search API processor plugin you need to create a class in the directory src/Plugin/search_api/processor (inside your custom module) that extends the ProcessorPluginBase class from the Search API module. The plugin definition is in the form of an annotation that appears at the top of the class.

<?php

namespace Drupal\my_module\Plugin\search_api\processor;

use Drupal\search_api\Processor\ProcessorPluginBase;

/**
 * Adds the item's view count to the indexed data.
 *
 * @SearchApiProcessor(
 *   id = "add_view_count",
 *   label = @Translation("View Count"),
 *   description = @Translation("Adds the items view count."),
 *   stages = {
 *     "add_properties" = 0,
 *   },
 *   locked = true,
 *   hidden = true,
 * )
 */
class AddViewCount extends ProcessorPluginBase {
}

There are a couple of methods that we need to create in order to get this working.

The first method needed tells the Search API module what properties are available. This needs to be called getPropertyDefinition() and should return an array of properties that this plugin defines. For the purposes of this situation we just need to ensure that a data source exists and define a single field called "View Count".

/**
 * {@inheritdoc}
 */
public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
  $properties = [];

  if ($datasource) {
    $definition = [
      'label' => $this->t('View Count'),
      'description' => $this->t('The view count for the item'),
      'type' => 'integer',
      'processor_id' => $this->getPluginId(),
    ];
    $properties['search_api_view_count'] = new ProcessorProperty($definition);
  }

  return $properties;
}

With the property in place, we can define the processor to inject the data into the field.

Before we can include the data from the node view counts module we first need to inject the service the module defines into our Search API plugin. This just uses the create() method that gets called when the plugin is created and injects the node view count service into the object.

/**
 * Node view count records manager.
 *
 * @var \Drupal\nodeviewcount\NodeViewCountRecordsManager
 */
protected $nodeViewCountRecordsManager;

/**
 * {@inheritdoc}
 */
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
  /** @var static $processor */
  $processor = parent::create($container, $configuration, $plugin_id, $plugin_definition);

  $processor->setNodeViewCountRecordsManager($container->get('nodeviewcount.records_manager'));

  return $processor;
}

/**
 * Sets the nodeviewcount.records_manager service.
 *
 * @param \Drupal\nodeviewcount\NodeViewCountRecordsManager $nodeViewCountRecordsManager
 *   The nodeviewcount.records_manager service.
 */
public function setNodeViewCountRecordsManager(NodeViewCountRecordsManager $nodeViewCountRecordsManager) {
  $this->nodeViewCountRecordsManager = $nodeViewCountRecordsManager;
}

With all that in place we can now define the method that is used to inject the value of the field into the search system. This method is called addFieldValues() and is passed an $item parameter which is a wrapped entity from Drupal. As the data we are interested in is only attached to nodes we just make sure that we are looking at a node before extracting the node view count for the node and adding that as a value. We then pull out the correct property from the search item (which is out view count field) and then inject our view count data into this field. It is also important to set some form of default here or you'll get blank or null in your results.

/**
 * {@inheritdoc}
 */
public function addFieldValues(ItemInterface $item) {
  $datasourceId = $item->getDatasourceId();
  if ($datasourceId == 'entity:node') {
    // This is a node entity so we need to find out if it has a view count.
    $node = $item->getOriginalObject()->getValue();
    $nodeViewCount = $this->nodeViewCountRecordsManager->getNodeViewsCount($node);

    if ($nodeViewCount) {
      // A view count was found, add it to the relevant field.
      $fields = $this->getFieldsHelper()->filterForPropertyPath($item->getFields(), NULL, 'search_api_view_count');
      foreach ($fields as $field) {
        if (isset($nodeViewCount[0])) {
          $field->addValue($nodeViewCount[0]->expression);
        }
        else {
          $field->addValue(0);
        }
      }
    }
  }
}

The plugin is now complete but it won't do anything unless we first plug it into the Solr search interface. To add this field visit the fields page for your index and click "Add fields", you should see the following in the list of fields that can be added.

Drupal Search API add custom field

When you click add in the above dialogue against this field it will be added to the list of set fields in your index.

Drupal Search API added field

The field is now ready to use, but remember that you must run a full re-index on the Sorl index for there to be any data available in it. Once that is done the new field is available to use as you would any other search field in search results or as a search filter.

There is one small thing missing from this setup though, which I hinted as being an optional hook before. The first time we index this field we get all of the correct values, but the values are only updated after this point if the node is re-saved. As a result we need to add a little bit of code to allow the view count to be updated when a node is viewed. To do this we use an implementation of hook_nodeviewcount_insert(), which is a hook defined by the node view count module itself. Using this hook we can react to the newly inserted view information and mark this node as needing to be updated in the search index. This is done by finding the available indexes for the entity we are looking at (ie, the node) and then calling the trackItemsUpdated() method on any available indexes that are found.

<?php

use Drupal\node\NodeInterface;
use Drupal\search_api\Plugin\search_api\datasource\ContentEntity;

/**
* Implements hook_nodeviewcount_insert().
*/
function my_module_nodeviewcount_insert(NodeInterface $node, $view_mode) {
 // A new view is about to be recorded so we need to tell solr to re-index this node.
 $indexes = ContentEntity::getIndexesForEntity($node);
 if (!$indexes) {
   return;
 }

 $nodeId = $node->id();

 $itemIds = [];
 foreach ($node->getTranslationLanguages() as $langcode => $language) {
   $itemIds[] = $nodeId . ':' . $langcode;
 }

 foreach ($indexes as $index) {
   $index->trackItemsUpdated('entity:node', $itemIds);
 }
}

Now, when a node is viewed the node count is updated in the database and then updated in the Solr search index. The trackItemsUpdated() method calls methods to update the index at the end of the page request, so in theory it shouldn't cause any slowdown or disruption to users.

I have skipped over some of the fine details in the implementation here, but the solution here will work for any sort of computed field. Also, although I have used this extensively on Drupal 8, I haven't not tried this on Drupal 9 yet due to the node view count module not having a Drupal 9 version. There is nothing about the rest of these implementation details that shouldn't work with other computed fields though. 

For completeness, here is the full code for the Search API plugin, along with all of the needed use statements that weren't included in the above examples.

<?php

namespace Drupal\my_module\Plugin\search_api\processor;

use Drupal\search_api\Datasource\DatasourceInterface;
use Drupal\search_api\Item\ItemInterface;
use Drupal\search_api\Processor\ProcessorPluginBase;
use Drupal\search_api\Processor\ProcessorProperty;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\nodeviewcount\NodeViewCountRecordsManager;

/**
 * Adds the item's view count to the indexed data.
 *
 * @SearchApiProcessor(
 *   id = "add_view_count",
 *   label = @Translation("View Count"),
 *   description = @Translation("Adds the items view count."),
 *   stages = {
 *     "add_properties" = 0,
 *   },
 *   locked = true,
 *   hidden = true,
 * )
 */
class AddViewCount extends ProcessorPluginBase {

  /**
   * Node view count records manager..
   *
   * @var \Drupal\nodeviewcount\NodeViewCountRecordsManager
   */
  protected $nodeViewCountRecordsManager;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    /** @var static $processor */
    $processor = parent::create($container, $configuration, $plugin_id, $plugin_definition);

    $processor->setNodeViewCountRecordsManager($container->get('nodeviewcount.records_manager'));

    return $processor;
  }

  /**
   * Sets the nodeviewcount.records_manager service.
   *
   * @param \Drupal\nodeviewcount\NodeViewCountRecordsManager $nodeViewCountRecordsManager
   *   The nodeviewcount.records_manager service.
   */
  public function setNodeViewCountRecordsManager(NodeViewCountRecordsManager $nodeViewCountRecordsManager) {
    $this->nodeViewCountRecordsManager = $nodeViewCountRecordsManager;
  }

  /**
   * {@inheritdoc}
   */
  public function getPropertyDefinitions(DatasourceInterface $datasource = NULL) {
    $properties = [];

    if (!$datasource) {
      $definition = [
        'label' => $this->t('View Count'),
        'description' => $this->t('The view count for the item'),
        'type' => 'integer',
        'processor_id' => $this->getPluginId(),
      ];
      $properties['search_api_view_count'] = new ProcessorProperty($definition);
    }

    return $properties;
  }

  /**
   * {@inheritdoc}
   */
  public function addFieldValues(ItemInterface $item) {
    $datasourceId = $item->getDatasourceId();
    if ($datasourceId == 'entity:node') {
      // This is a node entity so we need to find out if it has a view count.
      $node = $item->getOriginalObject()->getValue();
      $nodeViewCount = $this->nodeViewCountRecordsManager->getNodeViewsCount($node);

      if ($nodeViewCount) {
        // A view count was found, add it to the relevant field.
        $fields = $this->getFieldsHelper()->filterForPropertyPath($item->getFields(), NULL, 'search_api_view_count');
        foreach ($fields as $field) {
          if (isset($nodeViewCount[0])) {
            $field->addValue($nodeViewCount[0]->expression);
          }
          else {
            $field->addValue(0);
          }
        }
      }
    }
  }

}

 

Comments

Hei!
I think you have an error in getPropertyDefinitions.
In if it should be "if ($datasource)", not "if (!$datasource) "
 

Permalink

Thanks for letting me know Aleksi. It's been a while since I wrote that code, but what you say makes sense. If there is a datasource then we should perform some action on it. I've updated the code now.

Name
Philip Norton
Permalink

Nice code!! I am able to index the new custom field. Solr has inserted data as well. Now i am trying to fetch the data through Solr View, but I am seeing empty response even there is data in Solr. Do we need to add anything else?

Permalink

Hi bhanu,

I think I'll need to revisit this post as people do seem to be having problems with it. I don't have this project setup any more so I'll need to do that first before I can troubleshoot problems.

Did you find the solution in the end?

Name
Philip Norton
Permalink

Hello It's hard to find knowledgeable people for this subject, but you seem like you know what you're talking about! Thanks Velva

Permalink

Add new comment

The content of this field is kept private and will not be shown publicly.
CAPTCHA
5 + 8 =
Solve this simple math problem and enter the result. E.g. for 1+3, enter 4.
This question is for testing whether or not you are a human visitor and to prevent automated spam submissions.