Custom Image Search with Solr, Filefield Sources, and Ctools - Part 1

Over the years I have been a big fan and user of Apache Solr for search on Drupal sites, in particular using the Apache Solr Search set of modules, mostly because of its speed and ability to create facets for drilling down into search results, along with the flexibilty provided by the module API to customize the functionality as needed. While re-building the NewsBusters site from scratch in D7 last year, one of the issues that the users wanted to address was finding existing images to re-use in blog posts. The existing functionality used a file browsing functionality in the WYSIWYG, and as the number of images added to the site grew, just loading the browser to search for an image could take a few minutes. Another change we were making in the D7 version was storing all of our images on Amazon S3 (using the S3FS module), so I figured that I would take this chance to create a custom image search that used Solr. This would address speed issues, and would also allow the users to index metadata about images that could be used later to search for images.

One module that I have used in the past that allows some image searching functionality is Filefield Sources. For instance, it allows you to get images from remote URLs, search the files directory with an autocomplete textfield, and attach from a server directory. However, I needed to do something custom, and fortunately, the module implements hooks that allows you to create your own custom source. Fortunately the Autocomplete reference field does what I already need to do once I get the image name and fid, so all I need to do is create my search functionality and write the approriate values to the text field.

As I mentioned above, I also wanted to index metadata about the image to allow for better searching, so I needed to find a way to store that data. I initially tried extending the image field in code (with no luck), so after looking at the avaialable options, I settled on the Field Collection module. This allowed me to create a group of four fields for each image:

  • Image
  • Person
  • Organization
  • Year

So to start out, I create a custom module (nb_image_search), and I declare my custom source using hook_filefield_sources_info():

/**
 * Implements hook_filefield_sources_info().
 */
function nb_image_search_filefield_sources_info() {
  $source = array();
  $source['imgsearch'] = array(
    'name' => t('Image search with Solr'),
    'label' => t('Image Search'),
    'description' => t('Search for an existing image using Apache Solr'),
    'process' => 'nb_image_search_image_search_process',
    'value' => 'nb_image_search_image_search_value',
    'weight' => 1,
    'file' => 'includes/image_search.inc',
  );

  return $source;
}

The items in this array are:

  • name - the name of the option displayed in the image field settings for File Sources
  • label - The name of the option that is displayed on the node create/edit form.
  • description - The description of the source
  • process - the name of the process function that does all the heavy-work of creating a form element for searching and populating a field.
  • value - This callback function then takes the value of that field and saves the  file locally.
  • weight - Used for ordering the enabled sources on the node create screen.
  • file - The path to the file where the process and value functions are stored.

A second hook implementation that is needed is hook_theme():

/**
 * Implements hook_theme().
 */
function nb_image_search_theme() {
  return array(
    'nb_image_search_image_search_element' => array(
      'render element' => 'element',
      'file' => 'includes/image_search.inc',
    ),
  );
}

This specifies the theme function that will be used to theme the custom element.

Next up is the process function. This is basically a form function that defines the element for searching.

define('FILEFIELD_SOURCE_IMGSEARCH_HINT_TEXT', 'example.png [fid:123]');

/**
 * A #process callback to extend the filefield_widget element type.
 */
function nb_image_search_image_search_process($element, &$form_state, $form) {
  $element['imgsearch'] = array(
    '#weight' => 100.5,
    '#theme' => 'nb_image_search_image_search_element',
    '#filefield_source' => TRUE, // Required for proper theming.
    '#filefield_sources_hint_text' => FILEFIELD_SOURCE_IMGSEARCH_HINT_TEXT,
  );

  $element['imgsearch']['file_url'] = array(
    '#type' => 'textfield',
    '#maxlength' => NULL,
  );

  // Handle this being a Field Collection entity.
  if (isset($element['#entity']->is_new) && $element['#entity']->is_new == TRUE) {
    $nid = 0;
  }
  else {
    if (isset($element['#entity']->nid)) {
      $nid = $element['#entity']->nid;
    }
    elseif(isset($form_state['node']->nid)) {
      $nid = $form_state['node']->nid;
    }
  }

  // Get id for field within the field collection. Use the position of the languge value since it's right before the number.
  $language = $element['#language'];
  $lang_pos = strpos($element['#id'], $language);
  $id = !is_object($element['#file']) ? substr($element['#id'], $lang_pos + strlen($language) + 1, 1) : 0;

  $element['imgsearch']['search'] = array(
    '#type' => 'markup',
    '#markup' => '<div id="imgsearch">' . l("Search for Image", 'imgsearch/nojs/' . $id . '/' . $element['#bundle'], array('attributes' => array('class' => 'ctools-use-modal ctools-modal-imgsearch-modal-style'))) . '</div>'
  );

  $element['imgsearch']['select'] = array(
    '#name' => implode('_', $element['#array_parents']) . '_imgsearch_select',
    '#type' => 'submit',
    '#value' => t('Select'),
    '#validate' => array(),
    '#submit' => array('filefield_sources_field_submit'),
    '#name' => $element['#name'] . '[imgsearch][button]',
    '#limit_validation_errors' => array($element['#parents']),
    '#ajax' => array(
      'path' => 'file/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'],
      'wrapper' => $element['upload_button']['#ajax']['wrapper'],
      'effect' => 'fade',
    ),
  );

  return $element;
}

This creates the links and fields for the source as shown below.

Most of this is copied from the reference source, but there are a couple custom things going on here.

First, a Field Collection is a completely separate entity attached to the node, so it requires some custom code to get the node id.

Second, the $element['imgsearch']['search'] markup is a link to a custom page function (detailed in a later post) that generates the ctools modal window. There are two ctools classes applied to the link:

  • ctools-use-modal - opens the page callback being called in a modal window
  • ctools-modal-imgsearch-modal-style - a class to match custom settings for the modal that  defined in hook_node_prepare():
/**
 * Implementation of hook_node_prepare
 */
function nb_image_search_node_prepare($node) {
  if ($node->type == 'blog') {
    ctools_include('modal');
    ctools_modal_add_js();

    // Add custom settings for form size.
    drupal_add_js(array(
      'imgsearch-modal-style' => array(
        'modalSize' => array(
          'type' => 'fixed',
          'width' => 1000,
          'height' => 1200,
        ),
        'animation' => 'fadeIn',
        'closeText' => t('Close Search Window'),
        'loadingText' => t('Loading the Image Search window'),
      ),
    ), 'setting');
  }
}

When clicked on, this link will open up your modal window. Details of the window content will be covered in the next post.

Sep19