Friday, 19 April 2013

Epic battle for ApacheSolr VBO


I had a quite interesting challenge today. On one of our site we're using Solr search with the apachesolr Drupal module. (I know SearchApi knows more, we have our reason for apachesolr.) At one point we've got a request for bulk operations. I easily scored the issue to a simple module install, when realized there is no such thing as apachesolr and vbo.


Then the battle begins. I was first looking for issues and sandboxes. There are two official feature requests with some patches, but non of them is working and nowhere near to completion. Went to iRC and in the drupal solr channel it seemed nobody has heard about any solutions.

To clarify, for apachesolr there is the apachesolr_view module. It creates a new Views table type for each Solr index. If you add items to the index document then these views tables can fetch them and query them. It's really convenient, except that you don't have VBO in the views field list.

I was checking the VBO code and clearly it overrides the node and node revisions views table types. My first idea was to add a relationship to node by using the entity_id. It was a quite long process when I realized Solr is not SQL. *doh* So query alters are not working as expected - in fact bringing SQL and Solr together in a query is not really possible. Or you have to sacrifice all the chickens on Earth, maybe.

So there it comes the logical but tough way - make the apachesolr views tables to be VBO compatible. All right, every good story starts with a hook_views_api() implementation. Let's call the new glory module apachesolr_vbo. Here comes the first hook:

function apachesolr_vbo_views_api() {
  return array(
    'api' => 3,
    'path' => drupal_get_path('module', 'apachesolr_vbo') . '/views',
  );
}


As a proper citizen we add the views extensions under the ./views/ subfolder. There we create the base apachesolr_vbo.view.inc file where we alter the data that the apachesolr_views module defines:

function apachesolr_vbo_views_data_alter(&$data) {
  foreach (apachesolr_load_all_environments() as $env_id => $environment) {
    $apachesolr_base_table = 'apachesolr__' . $env_id;

    $data[$apachesolr_base_table]['views_bulk_operations'] = array(
      'title' => t('Bulk operations on indexed entities'),
      'group' => t('Bulk operations'),
      'help' => t('Provide a checkbox to select the row for bulk operations.'),
      'real field' => 'entity_id',
      'field' => array(
        'handler' => 'apachesolr_vbo_handler_field_operations',
        'click sortable' => FALSE,
      ),
    );
  }
}


We loop through all the Solr environments in order to have the VBO support on all. Hopefully all basic fields make sense, having the 'handler' on the top where we define the field handler class. At this point we can see the VBO field in views and we can add it to our table.

Field handlers are inherited from the base views_handler_field class that is defined in views. We want to save some time and reuse what is already defined in VBO, so we inherit from views_bulk_operations_handler_field_operations:

class apachesolr_vbo_handler_field_operations extends views_bulk_operations_handler_field_operations {
}


Now what is coming is a result of a long heuristically blood sweating multidimensional debugging session.

Take it deep breath, I'm about to shock you. The container for the solr result is not entity. It's basically a plain flat data structure. It's one of those times where you miss the Drupal API a lot. No worries, there is a workaround. We can imitate that the result is an entity. VBO uses the entity ID to fetch data for its operations. There is a small glitch, though. You can index more than one entity types. So it's better you have a nice one-entity-type index. I thought this would be best to add to the VBO field setting as a select box where you can set it to anything you want. You can extend the VBO field form by overriding options_form():

  public function options_form(&$form, &$form_state) {
    parent::options_form($form, $form_state);

    $entity_options = array();
    foreach (entity_get_info() as $machine_name => $entity_info) {
      $entity_options[$machine_name] = $entity_info['label'];
    }

    $form['vbo_settings']['apachesolr_vbo_entity_type'] = array(
      '#type' => 'select',
      '#options' => $entity_options,
      '#title' => t('Entity type mapping'),
      '#default_value' => $this->options['vbo_settings']['apachesolr_vbo_entity_type'],
      '#description' => t('The entity type the solr index should be mapped.'),
    );
  }


This will add a nice little select box to the form. We also must have to tell views that we have a new configuration property by implementing option_definition():

  public function option_definition() {
    $options = parent::option_definition();
    $options['vbo_settings']['contains']['apachesolr_vbo_entity_type'] = array(
      'default' => 'node',
    );
    return $options;
  }


And the reason we did this is to force define the entity mapping for the solr result record, let's then override the get_entity_type() function:

  public function get_entity_type() {
    return $this->options['vbo_settings']['apachesolr_vbo_entity_type'];
  }


We need one last workaround to issue the data type problem. When VBO creates the field (checkboxes) it needs to know which record it assigns to. Practically the checkbox should contain the ID of the entity. Now in our case it cannot be provided through the API (still not a real entity) in the new field (remember it's: 'views_bulk_operations') so we need to help it in hook_views_pre_render():

function apachesolr_vbo_views_pre_render(view $view) {
  if (strpos($view->base_table, 'apachesolr__') !== 0) {
    return;
  }

  foreach ($view->result as $idx => $result) {
    $view->result[$idx]->views_bulk_operations = $view->result[$idx]->entity_id;
  }
}


It's not much code, but the number of innocent kittens that hurt during the process is massive. Having said that, now the code is able to proxy the result to VBO and do whatever you want to do with it. Rules, actions, simple VBO thingies, you name it.

You can use this code, you just need to make it a complete module. I'll link my solution as soon as I created a Drupal sandbox project for it.

---

Peter

1 comment:

  1. And the link is: https://drupal.org/project/apachesolr_vbo

    ReplyDelete