Search API: Search nodes based on child node content

Published by Lennart Van Vaerenbergh on August 6, 2015

This post is about how to manipulate nodes so they can be found through the Drupal 7 search API based on content of their child nodes, also known as entity references.

Example:
You have the following content types
  • Food collection (parent node)
    • Title
    • Entity reference to Fruit node
  • Fruit ( child node)
    • Title (by searching on this field, the parent food collection is supposed to appear as result)
    • Taxonomy reference 'color' (by filtering on this field through facets, the parent food collection is supposed to appear as result)
The 'Food collection' has an entity reference field to the Fruit node. Say you want to display Food collections only in your search results and when searched on fruit fields, you want the parent 'Food collection' to be displayed. This is how it's done.

The key here is hook_entity_property_info_alter(). We can add new properties to our parent node which we can fill freely and will be picked up by the search index. The new properties will hold information of the child nodes and by that, the parent node can be searched based on child node content.

The following example gives an example of both a taxonomy list and a text string moved from the child nodes to the parent node. Add this code to your .module file of your custom module.

mymodule.module
/**
 * Implements hook_entity_property_info_alter().
 */
function mymodule_entity_property_info_alter(&$info) {
  // Extra property for food_collection containing the color terms of the child nodes.
  $info['node']['bundles']['food_collection']['properties']['child_colors'] = array(
    'label' => t('Colors of child nodes'),
    'type' => 'list<taxonomy_term>',
    'description' => t('Colors of the child nodes.'),
    'getter callback' => 'mymodule_get_child_node_data', // Callback to a custom function.
    'bundle' => 'food_color', // Machine name of taxonomy list.
  );

  // Extra property for food_collection containing the titles of the child nodes.
  $info['node']['bundles']['food_collection']['properties']['child_titles'] = array(
    'label' => t('Titles of child nodes'),
    'type' => 'text',
    'description' => t('Titles of child nodes.'),
    'getter callback' => 'mymodule_get_child_node_data', // Callback to a custom function.
  );
}

/**
 * Getter callback for the hook_entity_property_info_alter() implementation.
 */
function mymodule_get_child_node_data($node, $options, $name, $entity_type) {
  $parent_node = entity_metadata_wrapper('node', $node);
  
  switch ($name) {
    case 'child_colors':
      $list = array();

      // Loop through child fruit nodes.
      foreach ($parent_node->field_fruit_ref->getIterator() as $fruit_node) {
        // Loop through color terms of fruit node.
        foreach ($fruit_node->field_color->getIterator() as $color) {
          // Add term ID to list.
          $list[] = $color->getIdentifier();
        }
      }

      // Remove duplicate term IDs because some child nodes can have the same terms.
      $list = array_unique($list);
      
      // Return array of term IDs.
      return $list;

    case 'child_titles':
      $text = '';

      // Loop through child fruit nodes.
      foreach ($parent_node->field_fruit_ref->getIterator() as $fruit_node) {
        // Append fruit title to our text variable.
        $text .= $fruit_node->label() . ' ';
      }
 
      // Return string of node titles.
      return $text;
  }
  
  return;
}
I think the functions are self explanatory. The first function (the hook) defines the new properties of the parent content type 'food_collection'. The second function is the getter callback (pointed to in the first function) which will retrieve the actual data from the child nodes.
More information about the implementation of this hook and other types to define can be found here.

Note that I'm using the entity metadata wrapper class to access the node content. If you're not familiar with this, you can still access the node fields using the common way ($node['field_fruit_ref'][LANGUAGE_NONE][0]...).
Also note that, when using dpm() in this hook for debugging, it will appear when saving a content type. In case you wonder.

Now you've done all this and the Drupal cache is cleared, you can find these fields in the field configuration of your search index. Select the fields to be indexed and during the next indexing, the new properties of the food_collection node will be populated and indexed. You can also configure facets on them, just like on any other field.

Go forth and make a fancy search.

Add new comment

(If you're a human, don't change the following field)
Your first name.
(If you're a human, don't change the following field)
Your first name.
CAPTCHA
This challenge is for testing whether or not you are a human visitor and to prevent automated spam submissions.