background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount
Eric.London's picture

In this tutorial, I'll share my notes and code I've used to setup geospatial Apache Solr searching in Drupal 7 using the Search API module. For this tutorial I created a minimal Ubuntu server virtual machine. All the commands should be executed as a user with permission to modify files, or prefixed with "sudo".

The first thing I do with a fresh virtual machine is check for package upgrades.

$ apt-get update
$ apt-get upgrade

I find it cumbersome to type in a virtual machine window, so I'll install open-ssh and ssh from my Mac. If you plan to do so, you'll need to find your virtual machine's IP address using ifconfig. For this tutorial I added local DNS (/etc/hosts) to point "drupal7.vm" to my VM's IP.

$ apt-get install openssh-server

Install the LAMP stack. The following packages will install Apache httpd as a dependency.

$ apt-get install php5 php5-cli php5-common php5-curl php5-gd php5-mysql php-pear mysql-server

At this point, browsing to your VM/server's IP address will give you the standard Apache welcome message:
It works!
This is the default web page for this server.
The web server software is running but no content has been added, yet.

Install version control.

$ apt-get install git-core

Create a mysql database for Drupal 7.

$ mysql -u youruser -p
mysql> create database drupal7;
mysql> grant all privileges on drupal7.* to 'drupal7'@'localhost' identified by 'somepassword';
mysql> exit

Install drush via Pear.

$ pear upgrade-all
$ pear channel-discover pear.drush.org
$ pear install drush/drush

Verifying drush is installed.

$ which drush
/usr/bin/drush
$ drush --version
drush version 4.5

Create an Apache vhost directory

$ mkdir -p /var/www/vhosts

Download drupal via drush

$ cd /var/www/vhosts
$ drush dl drupal
# rename folder (as necessary)
$ mv drupal-7.10 drupal7

Integrate drupal file system with git

$ cd drupal7
$ git init
$ git add .
$ git commit -am "initial commit of drupal7"

Install drupal via drush

$ drush site-install standard --db-url=mysql://dbuser:pass@localhost/dbname

Add Apache2 vhost

$ cd /etc/apache2/sites-available
# create new file, called "drupal7" with contents:
<VirtualHost *:80>
  ServerName drupal7.vm
  DocumentRoot /var/www/vhosts/drupal7
  ErrorLog /var/log/apache2/drupal7-error_log
  CustomLog /var/log/apache2/drupal7-access_log combined
  <Directory /var/www/vhosts/drupal7>
    AllowOverride All
  </Directory>
</VirtualHost>

# create symlink
$ cd ../sites-enabled
$ ln -s ../sites-available/drupal7 001-drupal7.conf

# enable apache2 mod_rewrite module
$ a2enmod rewrite

# restart apache2
$ /etc/init.d/apache2 restart

At this point, browsing to your VM/server's hostname should show a Drupal installation.

Part 2, Tomcat/Solr

Installing java jdk and tomcat6

$ apt-get install openjdk-6-jdk tomcat6 tomcat6-admin tomcat6-common tomcat6-user

Browsing to your VM/server's hostname on port 8080 (ex: http://drupal7.vm:8080) will show the generic Tomcat welcome message:
It works !
If you're seeing this page via a web browser, it means you've setup Tomcat successfully. Congratulations!

Installing Solr in Tomcat

$ mkdir ~/downloads
$ cd ~/downloads
# Download the latest stable version of Apache Solr from:
url: http://www.apache.org/dyn/closer.cgi/lucene/solr/
# example:
$ wget http://www.motorlogy.com/apache//lucene/solr/3.5.0/apache-solr-3.5.0.tgz
$ tar -xzf apache-solr-3.5.0.tgz

Copy/rename java war file into Tomcat webapps directory

$ cp ~/downloads/apache-solr-3.5.0/dist/apache-solr-3.5.0.war /var/lib/tomcat6/webapps/solr.war

Note: copying the java war file into the Tomcat webapps folder will create this directory automatically:

/var/lib/tomcat6/webapps/solr

Copy solr files

$ cp -r ~/downloads/apache-solr-3.5.0/example/solr/ /var/lib/tomcat6/solr/

Create Catalina config file to link war file to solr directory

$ cd /etc/tomcat6/Catalina/localhost
# create new file: "solr.xml", with the contents:
<?xml version="1.0" encoding="UTF-8"?>
<Context docBase="/var/lib/tomcat6/webapps/solr.war" debug="0" privileged="true" allowLinking="true" crossContext="true">
<Environment name="solr/home" type="java.lang.String" value="/var/lib/tomcat6/solr" override="true" />
</Context>

Setup Tomcat admin user(s)

# edit file: /etc/tomcat6/tomcat-users.xml, ensure similar contents exist:
<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="admin"/>
<role rolename="manager"/>
<user username="eric" password="supersecretpassword" roles="admin,manager"/>
</tomcat-users>

Update webapps WEB-INF/web.xml file

# edit file: /var/lib/tomcat6/webapps/solr/WEB-INF/web.xml, update "solr/home" section to reflect solr path:
<env-entry>
  <env-entry-name>solr/home</env-entry-name>
  <env-entry-value>/var/lib/tomcat6/solr</env-entry-value>
  <env-entry-type>java.lang.String</env-entry-type>
</env-entry>

Download search api drupal modules that contain solr xml configuration files, and copy into solr conf directory

$ mkdir -p /var/www/vhosts/drupal7/sites/all/modules/contrib
$ cd /var/www/vhosts/drupal7/sites/all/modules/contrib
$ drush dl search_api search_api_solr
$ cp /var/www/vhosts/drupal7/sites/all/modules/contrib/search_api_solr/solrconfig.xml /var/lib/tomcat6/solr/conf/
$ cp /var/www/vhosts/drupal7/sites/all/modules/contrib/search_api_solr/schema.xml /var/lib/tomcat6/solr/conf/

Reset tomcat permissions, and restart tomcat

$ cd /var/lib
$ chown -R tomcat6.tomcat6 tomcat6
$ /etc/init.d/tomcat6 restart

You should now be able to browse to the solr admin java page.
Example: http://drupal7.vm:8080/solr/admin/
Solr Admin Page

If things aren't working well at this point, check the Tomcat logs and look for SEVERE log entries

/var/log/tomcat6/catalina.out

In addition, the solr java module should be listed in the Tomcat Web Application Manager
Ex URL: http://drupal7.vm:8080/manager/html

Part 3, Drupal code

Getting the solr-php-client library from code.google.com

$ mkdir -p /var/www/vhosts/drupal7/sites/all/libraries
$ cd /var/www/vhosts/drupal7/sites/all/libraries

# URL: http://code.google.com/p/solr-php-client/downloads/list
# File: SolrPhpClient.r60.2011-05-04.tgz
$ wget http://solr-php-client.googlecode.com/files/SolrPhpClient.r60.2011-05-04...
$ tar -xzf SolrPhpClient.r60.2011-05-04.tgz

Downloading and installing contrib drupal modules

$ cd /var/www/vhosts/drupal7
$ drush dl entity views ctools facetapi
$ drush en search_api search_api_views search_api_solr search_api_facetapi entity views views_ui ctools facetapi

(Optionally) I install devel, admin_menu, and disable overlay/toolbar

$ drush dl devel admin_menu
$ drush en devel admin_menu
$ drush dis overlay toolbar

Add the tomcat/solr server to Search API configuration.
- URL: /admin/config/search/search_api
- click on "+ Add Server"
- server name: Solr 3.5.0
- Service class: Solr service
- Solr host: localhost
- Solr port: 8080
- Solr path: /solr
- click Create Server

You should receive some confirmation messages:
The server was successfully created.
The Solr server could be reached (latency: # ms).
If not, ensure tomcat/solr is reachable at the url you specified and the tomcat service is running.

At this point Solr is ready to send/receive data and index content, but there is nothing to index. For this tutorial, I decided to build off of user profiles and store latitude and longitude using the geolocation field module.

$ drush dl geolocation
$ drush en geolocation

Adding some user profile fields:
- URL: /admin/config/people/accounts/fields
- First Name | field_name_first | Text
- Last Name | field_name_last | Text
- Geolocation | field_geolocation | Geolocation | Latitude/Longitude

I then added a bunch of users with latitude/longitude coordinates.
- URL: /admin/people/create
- note: I used Google Geocoding API to fetch the coordinates: http://code.google.com/apis/maps/documentation/geocoding/

Adding the search api index.
- URL: /admin/config/search/search_api
- click "+ Add index"
- Index name: People
- Item type: User
- Server: Solr 3.5.0
- click: Create Index

On the next admin page, you can select which fields to index. For this tutorial, I chose: User ID, Name, Email, URL, First Name, and Last Name. Unfortunately, at the time of writing this, the geolocation lat/lng fields are not exposed to the Entity API. I assume this is a temporary problem, and there are numerous patches in the geolocation issue queue.
@see (for example):
Property Info callback for Entity API - http://drupal.org/node/1366642
Fix for Search API not picking up the entity to index it's fields - http://drupal.org/node/1320564

I copied code directly from the issues queue, made some modifications, and created a custom module to expose the geolocation field data to the entity api module. In addition, I added a new property "lat_lon" that concatenates lat and lng together with a comma. @see: http://wiki.apache.org/solr/SpatialSearch

<?php
/**
* Implements hook_field_info_alter()
*/
function MYMODULE_field_info_alter(&$info) {
  if (isset(
$info['geolocation_latlng'])) {
   
$info['geolocation_latlng']['property_type'] = 'geolocation';
   
$info['geolocation_latlng']['property_callbacks'] = array('geolocation_property_info_callback');
  }
}

function
geolocation_property_info_callback(&$info, $entity_type, $field, $instance, $field_type) {
 
$name = $field['field_name'];
 
$property = &$info[$entity_type]['bundles'][$instance['bundle']]['properties'][$name];

 
$property['type'] = ($field['cardinality'] != 1) ? 'list<geolocation>' : 'geolocation';
 
$property['getter callback'] = 'entity_metadata_field_verbatim_get';
 
$property['setter callback'] = 'entity_metadata_field_verbatim_set';
 
$property['auto creation'] = 'geolocation_default_values';
 
$property['property info'] = geolocation_data_property_info();

  unset(
$property['query callback']);
}

function
geolocation_default_values() {

  return array(
   
'lat' => '',
   
'lng' => '',
   
'lat_sin' => '',
   
'last_name' => '',
   
'lat_cos' => '',
   
'lat_rad' => '',
   
'lat_lon' => '',
  );

}

function
geolocation_data_property_info($name = NULL) {

 
// Build an array of basic property information for the geolocation field.
 
$properties = array(
   
'lat' => array(
     
'label' => t('Latitude'),
    ),
   
'lng' => array(
     
'label' => t('Longitude'),
    ),
   
'lat_sin' => array(
     
'label' => t('Sine of Latitude'),
    ),
   
'lat_cos' => array(
     
'label' => t('Cosine of Latitude'),
    ),
   
'lat_rad' => array(
     
'label' => t('Radian Latitude'),
    ),
   
'lat_lon' => array(
     
'label' => t('Latitude,Longitude'),
    ),
  );

 
// Add the default values for each of the address field properties.
 
foreach ($properties as $key => &$value) {
   
    switch (
$key) {
   
      case
'lat_lon':
       
$value += array(
         
'description' => !empty($name) ? t('!label of field %name', array('!label' => $value['label'], '%name' => $name)) : '',
         
'type' => 'text',
         
'getter callback' => '_MYMODULE_geolocation_entity_property_verbatim_get',
         
'setter callback' => '_MYMODULE_geolocation_entity_property_verbatim_set',
        );
        break;
   
      default:
       
$value += array(
         
'description' => !empty($name) ? t('!label of field %name', array('!label' => $value['label'], '%name' => $name)) : '',
         
'type' => 'text',
         
'getter callback' => 'entity_property_verbatim_get',
         
'setter callback' => 'entity_property_verbatim_set',
        );
        break;
   
    }

}

return
$properties;
}


function
_MYMODULE_geolocation_entity_property_verbatim_get($data, array $options, $name, $type, $info) {
  if (
is_array($data) && isset($data['lat']) && isset($data['lng'])) {
    return
$data['lat'] . ',' . $data['lng'];
  }
  return
'';
}

function
_MYMODULE_geolocation_entity_property_verbatim_set(&$data, $name, $value, $langcode, $type, $info) {
 
// TODO
 
return;
}
?>

I added this code to a custom module, renamed function calls (as necessary), and enabled. Update the solr index to add the new fields to the index.
- URL: /admin/config/search/search_api/index/people/fields
- Expand "Add Related Fields"
- Choose Geolocation, click Add fields
The above will expose the following fields now available to the index:
- Geolocation » Latitude
- Geolocation » Longitude
- Geolocation » Sine of Latitude
- Geolocation » Cosine of Latitude
- Geolocation » Radian Latitude
- Geolocation » Latitude,Longitude
Enable "Geolocation » Latitude,Longitude" and save changes.

Index the content.
- URL: /admin/config/search/search_api/index/people/status
- Click: Index now
- note: if you had already indexed the content, you'll probably need to clear it first
In my environment, I got the following confirmation message:
Successfully indexed 7 items.

I find it to be very helpful to verify the xml response from Solr directly after making changes to the index/schema.
The following URL structure will query solr for all results and return all fields:

Ex URL: http://drupal7.vm:8080/solr/select/?q=&fl=*

A sample XML document response.

<doc>
  <str name="f_ss_search_api_language"/>
  <str name="f_ss_url">http://drupal7.vm/user/3</str>
  <str name="id">people-3</str>
  <str name="index_id">people</str>
  <long name="is_uid">3</long>
  <str name="item_id">3</str>
  <arr name="spell">
    <str>nashua</str>
    <str>nashua@example.com</str>
    <str>nashua</str>
    <str>nashua</str>
    <str>42.933692,-72.278141</str>
  </arr>
  <str name="ss_search_api_id">3</str>
  <str name="ss_search_api_language"/>
  <str name="ss_url">http://drupal7.vm/user/3</str>
  <arr name="t_field_geolocation:lat_lon">
    <str>42.933692,-72.278141</str>
  </arr>
  <arr name="t_field_name_first">
    <str>nashua</str>
  </arr>
  <arr name="t_field_name_last">
    <str>nashua</str>
  </arr>
  <arr name="t_mail">
    <str>nashua@example.com</str>
  </arr>
  <arr name="t_name">
    <str>nashua</str>
  </arr>
</doc>

Take note the field name in the following XML, it is used in the next file edit.

<arr name="t_field_geolocation:lat_lon">
  <str>42.933692,-72.278141</str>
</arr>

Update the solr schema.xml configuration and add the geospatial fieldType and field data.

# Edit file: /var/lib/tomcat6/solr/conf/schema.xml
# Just prior to the closing "</types>" tag, I inserted: (around line 287)
    <fieldType name="point" class="solr.PointType" dimension="2" subFieldSuffix="_d"/>
    <fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>
    <fieldtype name="geohash" class="solr.GeoHashField"/>

# And, just after the opening "<fields>" tag, I inserted:
    <field name="t_field_geolocation:lat_lon" type="location" indexed="true" stored="true"/>
    <dynamicField name="*_coordinate"  type="tdouble" indexed="true"  stored="false"/>

Restart Tomcat

$ /etc/init.d/tomcat6 restart

Since the schema and solr data types have been updated, the content will have to be re-indexed.
- URL: /admin/config/search/search_api/index/people/status
- click: Clear index
- click: Index now

Returning to the solr query above will now show updated xml: (note: no longer an array)

<str name="t_field_geolocation:lat_lon">42.933692,-72.278141</str>

Verify the native solr geospatial searching is working using the following query syntax:
URL: http://drupal7.vm:8080/solr/select/?q=&fl=*&fq={!geofilt sfield=t_field_geolocation:lat_lon pt=42.933692,-72.278141 d=100}
By putting a distance parameter of 100 (kilometers) and Nashua NH coordinates, I get 2 results: Nashua and Portsmouth, awesome.

Create a solr integrated view.
- URL: /admin/structure/views/add
- View name: People
- Show: People
- Create a Page [checked]
- Path: people
- Continue & edit
Note: at this point, you have full reign over view configuration. For this tutorial, I set the format to Grid, and added some fields:
- Geolocation: Latitude,Longitude (indexed)
- Indexed User: Email
- Indexed User: First Name
- Indexed User: Last Name
- Indexed User: Name
Save the view when edits are complete.

Browsing to the view will show something like this:
Ex URL: http://drupal7.vm.people
People View

The next chunk of custom code modifies the solr query executed and adds geospatial filtering.
@see: hook_search_api_solr_query_alter(array &$call_args, SearchApiQueryInterface $query)

<?php
function MYMODULE_search_api_solr_query_alter(array &$call_args, SearchApiQueryInterface $query) {

 
$lat = 42.933692;
 
$lng = -72.278141;
 
$distance = 100;

 
$call_args['params']['fq'][] = "{!geofilt sfield=t_field_geolocation:lat_lon pt={$lat},{$lng} d={$distance}}";

}
?>

The above code will limit the view's results using the hardcoded coordinates.
People View 2

Clearly, it works but there are loose ends to tie..
- automatically fetch a user's coordinates to store in the geolocation field
- add a search form to the people view page to allow the user to search for a location (instead of hard coded coordinates, blah)
- translate the user's location search input to coordinates using an API

Hopefully, I can find more time to elaborate on this tutorial in the near future! Cheers.

In this quick snippet, I'll show some code that uses the Tika Java library to index the content in WebFM file attachments and add the data as Apache Solr documents. This code is designed to work with the Apache Solr Search Integration Drupal module, and piggyback off of the Apache Solr Attachments module. Out of the box, the Apache Solr Attachments module can index CCK file fields and node file attachments, but the WebFM module uses its own custom file tables and therefore the files are not indexed. This code assumes you have the Tika library already integrated with your Solr installation. Please review the Tika Getting Started documentation for more information.

<?php
/**
* Implements hook_update_index()
*/
function index_webfm_update_index() {

 
$cron_limit = 10;
 
$rows = apachesolr_get_nodes_to_index('index_webfm', $cron_limit);
 
$success = apachesolr_index_nodes($rows, 'index_webfm', '_index_webfm_add_documents');

}

/**
* Implements custom function to add webfm file attachments content as Solr documents
*/
function _index_webfm_add_documents(&$documents, $nid, $namespace = 'index_webfm') {
  if (
$namespace != 'index_webfm') {
    return;
  }

 
// load node
 
$node = node_load($nid);
  if (!
is_object($node)) {
    return;
  }

 
// get webfm fids
 
$sql = "
    select a.fid, f.fpath as `filepath`, f.fmime as `filemime`, f.fcreatedate as `timestamp`
    from {webfm_attach} a
    join {webfm_file} f on f.fid = a.fid
    where a.nid = %d
    order by a.weight
  "
;
 
$resource = db_query($sql, $node->nid);
 
$webfm_data = array();
  while (
$row = db_fetch_object($resource)) {
   
$webfm_data[] = $row;
  }
 
 
// ensure webfm files exists for node
 
if (!count($webfm_data)) {
    return;
  }
 
 
// loop through web fm data, collect data to add to document
 
$tika_data = NULL;
  foreach (
$webfm_data as $file) {
   
   
// direct tika
   
if (variable_get('apachesolr_attachment_extract_using', 'tika') == 'tika') {
     
      if (
function_exists('apachesolr_attachments_extract_using_tika')) {
       
$tika_data = apachesolr_attachments_extract_using_tika($file->filepath);
      }
     
    }
   
// tika via solr
   
else {
     
      if (
function_exists('apachesolr_attachments_extract_using_solr')) {
        list(
$tika_data, $metadata) = apachesolr_attachments_extract_using_solr($file->filepath);
      }
     
    }
    if (!
$tika_data || !is_string($tika_data)) {
      continue;
    }
   
   
// create new Solr document
   
$document = new Apache_Solr_Document();
   
$document->id = apachesolr_document_id($file->fid .'-'. $node->nid, 'file');
   
$document->url = file_create_url($file->filepath);
   
$document->path = $file->filepath;
   
$document->hash = apachesolr_site_hash();
   
$document->entity = 'file';
   
$document->site = url(NULL, array('absolute' => TRUE));
   
$document->nid = $node->nid;
   
$document->title = basename($file->filepath);
   
$document->created = apachesolr_date_iso($file->timestamp);
   
$document->changed = $document->created;
   
$document->status = $node->status;
   
$document->sticky = $node->sticky;
   
$document->promote = $node->promote;
   
$document->uid = $node->uid;
   
$document->name = $node->name;
   
$document->body = apachesolr_clean_text(basename($file->filepath) . ' ' . $tika_data);
   
$document->ss_filemime = $file->filemime;
   
$document->ss_file_node_title = apachesolr_clean_text($node->title);
   
$document->ss_file_node_url = url('node/' . $node->nid, array('absolute' => TRUE));
  
   
// add new webfm document to documents
   
$documents[] = $document;
       
  }
}
?>

After implementing this code, when nodes were set to be indexed by Solr, their webfm file attachments were separately processed, the content was extracted from the file attachments, and added as new Solr documents.

In this article, I'll address an issue that has surfaced on every recent project I've been working on: how to update nodes when a CCK field's allowed values list changes. For my projects, I implemented a variety of batch processes and drush scripts to solve this problem. But for this article, I decided to implement a form submit handler solution to automatically update nodes when the CCK field settings page is submitted. Please be careful when implementing code like this, it has potential to update a lot of nodes, and hit memory limitation issues depending on how many nodes need to be updated. As I mentioned before, I implemented a combination of batch APIs and drush scripts to process production environments; I implemented this code in a development environment. Anyway..

I created a new content type "band", and added a CCK text/select field (genre) with an allowed values list:

Allowed value list before

I used Devel's auto-generate content functionality to create 20 nodes (/admin/generate/content), and then added a simple view to show the node's title and genre:

view of nodes before

Now that I had some working test data, I added some code to a custom module to modify the CCK field property settings page, and add a submit handler update the nodes:

<?php
// define module constants
define('CCK_FIELD_PROCESS', 'field_genre');
define('CCK_TYPE_PROCESS', 'band');

/**
* Implements hook_form_alter()
*/
function helper_form_alter(&$form, &$form_state, $form_id) {
 
 
// check for CCK form, CCK type, and CCK field (see above constants)
 
if ($form_id == 'content_field_edit_form' && $form['type_name']['#value'] == CCK_TYPE_PROCESS && $form['field_name']['#value'] == CCK_FIELD_PROCESS) {
   
   
// get original allowed values
   
$original_allowed_values_string = $form['field']['allowed_values_fieldset']['allowed_values']['#default_value'];
   
   
// explode values, get array of values   
   
$exploded = explode("\n", $original_allowed_values_string);
   
$original_allowed_values_array = array();
    foreach (
$exploded as $data) {

     
// explode data on "|"
     
list($key, $value) = $exploded2 = explode('|', $data);     
     
$original_allowed_values_array[$key] = $value;
     
    }
   
   
// ensure data exists
   
if (empty($original_allowed_values_array)) {
      return;
    }

   
// store original values in form state storage
   
$form_state['storage']['original_allowed_values'] = $original_allowed_values_array;
   
   
// store other information in form state
   
$form_state['storage']['cck_type'] = CCK_TYPE_PROCESS;
   
$form_state['storage']['cck_field'] = CCK_FIELD_PROCESS;

   
// add submit handler
   
$form['#submit'][] = '_helper_form_alter_content_field_edit_form_submit';
       
  }

}

/**
* Implements custom form submit handler
*/
function _helper_form_alter_content_field_edit_form_submit($form, &$form_state) {

 
// get new allowed values list
 
$new_allowed_values_string = $form_state['values']['allowed_values'];
 
 
// ensure data exists
 
if (empty($new_allowed_values_string)) {
    return;
  }

 
// explode values, get array of values   
 
$exploded = explode("\n", $new_allowed_values_string);
 
$new_allowed_values_array = array();
  foreach (
$exploded as $data) {

   
// explode data on "|"
   
list($key, $value) = $exploded2 = explode('|', $data);     
   
$new_allowed_values_array[$key] = $value;
   
  } 

 
// ensure data exists
 
if (empty($new_allowed_values_array)) {
    return;
  }
 
 
// NOTE: the next few lines gets the key values from the allowed values
  // and checks to see which are different.
  // This code assumes any change to the keys (including changing the order) will update the nodes.
  // Update to your heart's content
 
$new_keys = array_keys($new_allowed_values_array);
 
$old_keys = array_keys($form_state['storage']['original_allowed_values']);
 
 
$key_diffs = array();
  foreach (
$old_keys as $key => $value) {
   
    if (
$new_keys[$key] != $value) {
     
$key_diffs[$value] = $new_keys[$key];
    }
   
  }
 
 
// ensure data has changed
 
if (empty($key_diffs)) {
    unset(
$form_state['storage']);
    return;
  }
 
 
// get data from form storage
 
$cck_field = $form_state['storage']['cck_field'];
 
$cck_type = $form_state['storage']['cck_type'];
 
 
// loop through key diffs
 
foreach ($key_diffs as $old => $new) {
 
   
// get a list of node ids that need to be updated
    // NOTE: this sql assumes the field is NOT shared
   
$sql = "
      select n.nid
      from {node} n
      join {content_type_%s} b on b.nid = n.nid and b.vid = n.vid
      where n.type = '%s' and b.%s_value = '%s'
    "
;
   
$resource = db_query($sql, $cck_type, $cck_type, $cck_field, $old);
   
$node_ids = array();
    while (
$row = db_fetch_object($resource)) {
     
$node_ids[] = $row->nid;
    }
   
   
// ensure results exist
   
if (empty($node_ids)) {
      continue;
    }
   
   
// loop through node ids
    // NOTE: got a lot of nodes? you might need a batch API instead
   
foreach ($node_ids as $nid) {
     
     
// load node
     
$node = node_load($nid, NULL, TRUE);
     
     
// ensure node loaded
     
if (!is_object($node)) {
        continue;
      }
     
     
// update node properties
     
$node->{$cck_field}[0]['value'] = $new;
     
$node->revision = TRUE;
     
node_save($node);
     
     
// remove cck cache for node
     
db_query("DELETE FROM {cache_content} WHERE cid LIKE 'content:%d%'", $node->nid);
     
    }
 
  }
 
 
// remove form storage
 
unset($form_state['storage']);

}
?>

To test this code, I went back to the CCK field settings page (example: /admin/content/node-type/band/fields/field_genre), updated the allowed value list, and saved.

updated allowed value list

The form submit handler is triggered and the nodes are updated. Refreshing my view shows the updated genre fields on my test nodes:

Updated node list

Eric.London's picture

I've encountered this issue a few times recently when working with Apache Solr (in Drupal 6): clients want to to show all search results and allow the users to browse facets, instead of requiring a search phrase or filter to begin the search. The current version of the Apache Solr module appears to require at least one of these items.

A quick look at apachesolr_search.module, in the function: apachesolr_search_view() shows this is not going to happen:

<?php
$keys
= trim(search_get_keys());
// ...snip...
$filters = trim($_GET['filters']);
// ...snip...
if ($keys || $filters) {
 
// do search
}
?>

So, what if there was a way to modify the $_GET values prior to the apache solr search page callback? hook_boot maybe? NOTE: hook_boot functions are only processed if the bootstrap column in the system table for your module is set to 1! Example SQL to update..

update {system}
set bootstrap = 1
where type = 'module' and name = 'MYMODULE'

I then created this hook_boot function in my module. It is run prior to the the apache solr search page callback, and allows me to tamper with $_GET variables. In this case, I just add a fake filter to allow the search to pass the above conditional statements.

<?php
function MYMODULE_boot() {
 
 
// check for search page
 
$search_path = 'search/apachesolr_search';
  if (
substr($_GET['q'], 0, strlen($search_path))==$search_path) {
   
   
// get search phrase
   
$search_phrase = substr($_GET['q'], strlen($search_path)+1);
   
   
// get filters
   
$filters = $_GET['filters'];
   
   
// check if filters AND search phrase do not exist
   
if (empty($filters) && empty($search_phrase)) {
         
     
// set fake filter
     
$_GET['filters'] = 1;
     
    }
  }
 
}
?>

Now, going to the solr search page defaults to show all results and facets:

Solr All Results

It appears to work, but anyone know of a better way to accomplish this?! :)

In this article, I'll share some code that I wrote that uses Appcelerator Titanium to create a simple iPhone application that communicates with a Drupal site via SOAP. From the iPhone side of things I used the Suds Javascript SOAP client, and from Drupal, I created a nusoap soap server instance.

Let's start with the SOAP server instance. I created a module called "nusoap", and copied the lib folder from nusoap into it. I then created a file in the module's directory called "soap-server.php" and added the following code. For the sake of this article, I did not include the rest of the Drupal module's code.

<?php
// $Id$

// define namespace
define('NUSOAP_NAME_SPACE', 'erl.dev');

// define path to nusoap library file
$nu_soap_path = 'lib/nusoap.php';

// ensure nu_soap library exsists
if (!file_exists($nu_soap_path)) {
  die(
'An error has occurred initializing the soap server.');
}

// include nu_soap library
require_once ($nu_soap_path);

// create new soap server instance
$soap_server = new nusoap_server();

// configure wsdl
$soap_server->configureWSDL(NUSOAP_NAME_SPACE, 'urn:'. NUSOAP_NAME_SPACE);

$soap_server->register(
 
// method name
 
'login_fetch_data',
 
// input args
 
array(
   
'name' => 'xsd:string',
   
'pass' => 'xsd:string',
  ),
 
// output args
 
array(
   
'return' => 'xsd:string'
 
),
 
// namespace
 
'uri:'. NUSOAP_NAME_SPACE,
 
// SOAPAction
 
'uri:'. NUSOAP_NAME_SPACE .'#login_fetch_data',
 
// style
 
'rpc',
 
// use
 
'encoded'
);

// change directory to docroot
chdir($_SERVER['DOCUMENT_ROOT']);

// drupal bootstrap
require_once './includes/bootstrap.inc';
drupal_bootstrap(DRUPAL_BOOTSTRAP_FULL);

// process raw post data
$HTTP_RAW_POST_DATA = isset($HTTP_RAW_POST_DATA) ? $HTTP_RAW_POST_DATA : '';
$soap_server->service($HTTP_RAW_POST_DATA);

/**
* Define soap methods
*/

function login_fetch_data($name, $pass) {
 
 
$params = array(
   
'name' => $name,
   
'pass' => $pass,
  );
 
 
$account = user_authenticate($params);
 
 
// check for error
 
if (!is_object($account)) {
   
// handle authentication error here
    // return something
 
}
   
 
// return user uid, or some other piece of data
 
return $account->uid;
 
}
?>

The above code registers one soap server method "login_fetch_data" which accepts the arguments (name and pass), loads a full Drupal bootstrap, authenticates the user, and returns back the user's uid. By browsing to the soap server file directly, I can see nusoap's WSDL definition: (example: http://drupal.vm/sites/all/modules/custom/nusoap/soap-server.php)

SOAP Server Methods

To test if the soap server method was working, I used the follow PHP code snippet (which I pulled out of a test page callback):

<?php
// define path to nu_soap library, relative to module directory
define('NUSOAP_PATH', drupal_get_path('module', 'nusoap') .'/lib/nusoap.php');

// define namespace
define('NUSOAP_NAME_SPACE', 'erl.dev');

// include nu_soap library
require_once(NUSOAP_PATH);

// define wsdl path
$wsdl_path = 'http://' . $_SERVER['HTTP_HOST'] . base_path() . drupal_get_path('module', 'nusoap') . '/soap-server.php?wsdl';

// create new soap client instance
$soap_client = new nusoap_client($wsdl_path, true);

// check for error
$error = $soap_client->getError();
if (
$error) {
 
// handle error
}
 
$args = array(
 
'name' => 'Eric.London',
 
'pass' => 'super secret password',
);

// call soap server method
$result = $soap_client->call('login_fetch_data', $args);

$output = "";
$output .= "<pre>";
$output .= "SENT: ";
$output .= print_r($args, true);
$output .= "RECEIVED: ";
$output .= print_r($result, true);
$output .= "</pre>";
   
return
$output;
?>

I then created a new Titanium iPhone project. In the Resources directory, I created a directory called "soap-test-includes" to contain the suds.js library, and the soap.js file which uses this library. The suds.js file is available in the Kitchen Sink Titanium project, if you choose to import it.

Titanium folder structure

I added the following code to the app.js file:

// this sets the background color of the master UIView (when there are no windows/tab groups on it)
Titanium.UI.setBackgroundColor('#000');

// create tab group
var tabGroup = Titanium.UI.createTabGroup();

//
// create base UI tab and root window
//
var win1 = Titanium.UI.createWindow({ 
    title:'Soap Test',
    backgroundColor:'#fff',
    url: 'soap-test-includes/soap.js'
});
var tab1 = Titanium.UI.createTab({ 
    icon:'KS_nav_views.png',
    title:'Soap Test',
    window:win1
});

//
//  add tabs
//
tabGroup.addTab(tab1); 

// open tab group
tabGroup.open();

And, I added the following code to the soap.js include file:

// include suds soap library
Titanium.include('suds.js');

var window = Ti.UI.currentWindow;
var label = Ti.UI.createLabel({
  top: 10,
  left: 10,
  width: 'auto',
  height: 'auto',
  text: 'Contacting Drupal soap server...\n'
});

window.add(label);

// define soap params
var url = "http://drupal.vm/sites/all/modules/custom/nusoap/soap-server.php";
var callparams = {
  name: 'Eric.London',
  pass: 'super secret password'
};

// create new suds soap client
var suds = new SudsClient({
  endpoint: url,
  targetNamespace: 'erl.dev'
});

try {
 
  // make soap server call
  suds.invoke('login_fetch_data', callparams, function(xmlDoc) {
 
    // fetch results from method
    results = xmlDoc.documentElement.getElementsByTagName('login_fetch_dataResponse');
      
    // check if results exits
    if (results && results.length>0) {
 
      // output results
      label.text += "User uid: " + results.item(0).text;     

    }
    else {
      // handle error    
    }

  });
} catch(e) {
  Ti.API.error('Error: ' + e);
}

When I build and run the project, the iOS Simulator opens and shows the results of the soap connection!

iOS soap connection

As you can see, Titanium allows you to write an iPhone application without touching the native Objective-C code. In this example, all the code was written in javascript, with a PHP backend. Much of the javascript code I used is based on code found in the Kitchen Sink project; I suggest you check it out :)

Eric.London's picture

In this article, I'll share some code that I use to gray out the screen during AJAX calls to prevent users from clicking on anything, and displays a visual cue to the user that their click request is still being processed.

In the following code, I created a hook_menu() implementation and 2 page callbacks; one to show a clickable link and the other to process the AJAX request.

<?php
/**
* Implements hook_menu()
*/
function helper_menu() {

 
$items = array();

 
// define the page callback to show the clickable link 
 
$items['shadow-test'] = array(
   
'title' => t('Shadow Test'),
   
'page callback' => '_helper_page_callback_shadow_test',
   
'access arguments' => array('access content'),
   
'type' => MENU_CALLBACK,
  );
 
 
// define the page callback to process the AJAX request
 
$items['shadow-test-ajax'] = array(
   
'title' => t('Shadow Test Ajax'),
   
'page callback' => '_helper_page_callback_shadow_test_ajax',
   
'access arguments' => array('access content'),
   
'type' => MENU_CALLBACK,
  );
 
  return
$items;

}

/**
* Implements the page callback to show the clickable link
*/
function _helper_page_callback_shadow_test() {

 
// include module css
 
drupal_add_css(drupal_get_path('module', 'helper') .'/helper.css');

 
// include module javascript
 
drupal_add_js(drupal_get_path('module', 'helper') .'/helper.js');

 
// generate shadow html
 
$shadow_html = "<div id='shadow' style='background: url(\"" . base_path() . drupal_get_path('module', 'helper') .'/shadow.png' . "\")'></div>";

 
// define javascript variables to be passed to the DOM
 
$js_vars = array(
   
'helper' => array(
     
'ajax_path' => base_path() . 'shadow-test-ajax',
     
'shadow_html' => $shadow_html,
    ),
  );

 
// pass variables to javascript
 
drupal_add_js($js_vars, 'setting');

 
// create a variable for page output
 
$output = "";
 
 
// create a clickable link
  // NOT: this link will be overwritten via jQuery
 
$output .= l(
   
t('Click Me'),
   
$_REQUEST['q'],
    array(
     
'attributes' => array(
       
'class' => 'ajax_clickable',
      ),
    )
  );
 
 
// return page output
 
return $output;

}

/**
* Implements page callback to process the AJAX request
*/
function _helper_page_callback_shadow_test_ajax() {

 
// wait 2 seconds
  // NOTE: this line is just used to demo the "shadow" effect
 
sleep(3);
 
 
// return a JSON value
 
$ret = new StdClass();
 
$ret->status = true;
  print
drupal_json($ret);

  die;

}
?>

After flushing my menu cache, and browsing to the first page callback, I see the following:

Shadow Off

I added some CSS to my module's CSS include file (helper.css), to help position the "shadow" full screen:

#shadow {
  z-index: 1000;
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: none;
}

I created a 50% black transparent PNG image in PhotoShop:

50% black image

I added some jQuery to my module's javascript include file (helper.js), to add the click event to the link, and two functions to hide and show the shadow.

Drupal.behaviors.helper = function(context) {

  // find the link with the matching class, and add a click event
  $('.ajax_clickable').click(function(){
   
    // show shadow
    $(this).show_shadow();

    // make ajax call   
    $.getJSON(
      Drupal.settings.helper.ajax_path,
      function(data) {
       
        // when the ajax call is done, hide the shadow
        $(this).hide_shadow();
       
      }
    );

    // prevent the a tag from actually going anywhere
    return false;
 
  });
 
}

;(function($) {

  // defines the function to show the shadow
  $.fn.show_shadow = function() {
 
    // ensure the shadow does not already exist
    if ($('body #shadow').length == 0) {
     
      // add shadow html, and fade it in
      $('body').append(Drupal.settings.helper.shadow_html).find('#shadow').fadeIn();
         
    }
 
  }

  // defines the function to hide the shadow
  $.fn.hide_shadow = function() {
 
    // fade it out, and then remove it from the dom
    $('#shadow').fadeOut('slow', function() {
      $(this).remove();
    });
 
  }
 
})(jQuery);

Now, when I click on the link the page fades to black and the user cannot click on anything. After the 3 second delay (defined in the AJAX callback), the black shadow fades away.

Shadow On

I've been working on a few Drupal projects recently that required geospatial Solr integration. After numerous failed attempts with patches, outdated modules, and Java Solr plugins, I decided to try a different approach: upgrading the Solr library from 1.4.1 to 3.1.0. I wish I had seen this earlier, but spatial search is core to Solr 3.1, see: http://wiki.apache.org/solr/SpatialSearch

This article is a companion to my configuration documented here: http://ericlondon.com/creating-centos-server-installation-apache-mysql-t..., and assumes you have a similar working environment using the 1.4.1 Solr library.

I started by downloading apache-solr-3.1.0.tgz and replacing my installed Solr library 1.4.1 with the new files:

$ tar -xzf apache-solr-3.1.0.tgz

# copy/rename solr war file into Tomcat webapps directory
$ cp ~/downloads/apache-solr-3.1.0/dist/apache-solr-3.1.0.war /var/lib/tomcat6/webapps/solr.war

# copy solr files
$ cp -r ~/downloads/apache-solr-3.1.0/example/solr/ /var/lib/tomcat6/solr/

I then re-copied the Drupal apachesolr module xml configuration files into my Tomcat Solr directory:

$ cp /var/www/vhosts/example.com/sites/all/modules/contrib/apachesolr/protwords.txt /var/lib/tomcat6/solr/conf/
$ cp /var/www/vhosts/example.com/sites/all/modules/contrib/apachesolr/schema.xml /var/lib/tomcat6/solr/conf/
$ cp /var/www/vhosts/example.com/sites/all/modules/contrib/apachesolr/solrconfig.xml /var/lib/tomcat6/solr/conf/

Unfortunately, the Drupal apachesolr module and provided xml configuration files do not account for the new data types in 3.1.0. I modified my schema.xml file and added the following:

<fieldType name="point" class="solr.PointType" dimension="2" subFieldSuffix="_d"/>
<fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>
<fieldtype name="geohash" class="solr.GeoHashField"/>

<field name="coordinates" type="location" indexed="true" stored="true"/>
<dynamicField name="*_coordinate"  type="tdouble" indexed="true"  stored="false"/>

Next, I added a hook_apachesolr_update_index() function to a custom module to index Location CCK data into the Solr Document.

<?php
function MYMODULE_apachesolr_update_index(&$document, $node) {

 
// check for location data
 
if (empty($node->field_location[0]['latitude']) || empty($node->field_location[0]['longitude'])) {
    return;
  }
   
 
$document->coordinates = $node->field_location[0]['latitude'] . ',' . $node->field_location[0]['longitude'];
 
}
?>

I then created a bunch of nodes that had a Location CCK field, and varied their locations around New England. After re-indexing my Solr index and running cron, I queried Solr directly to ensure the locative data was added to the documents.

Solr Query Coordinates

I then used the new spatial query syntax (documented here: http://wiki.apache.org/solr/SpatialSearch) to pass in Boston coordinates. The results were limited to 3 matches within the specified distance.

Example query syntax, with coordinates and distance, searching for "ma"

?q=ma&fq={!geofilt sfield=coordinates pt=42.346617,-71.098747 d=10}

Solr Query GeoSpatial Search

The next challenge was integrating with the Drupal apachesolr module. The spatial query syntax has an augmented "fq" value, but the apachesolr Drupal module sets the fq parameter automatically (via: $query->get_fq()):

<?php
# snippet from file: apachesolr.module
function apachesolr_modify_query(&$query, &$params, $caller) {
 
// ...snip...

  // TODO: The query object should hold all the params.
  // Add array of fq parameters.
 
if ($query && ($fq = $query->get_fq())) {
   
$params['fq'] = $fq;
  }
 
// ...snip...
?>

After much struggle, I changed one line in the apachesolr.module file to allow the fq value to be modified (I normally refuse to patch ANY contrib module… please, if anyone knows a way to do this without patching, please let me know! :)

<?php
function apachesolr_modify_query(&$query, &$params, $caller) {
 
// ...snip...

  // TODO: The query object should hold all the params.
  // Add array of fq parameters.
 
if ($query && ($fq = $query->get_fq())) {
   
$params['fq'] = array_merge($fq, $params['fq']);
  }
 
// ...snip...
?>

Actual diff:

1264c1264
<     $params['fq'] = $fq;
---
>     $params['fq'] = array_merge($fq, $params['fq']);

I then add a hook_apachesolr_modify_query() function to my custom module to override the Solr query params.

<?php
function MYMODULE_apachesolr_modify_query(&$query, &$params, $caller) {

  if (
$caller != 'apachesolr_search') {
    continue;
  }

 
// NOTE: hardcoded Boston area coordinates: 
 
$coordinates = '42.346617,-71.098747'

 
$distance = 10;
 
 
// modify params
 
$params['fq'][] = "{!geofilt sfield=coordinates pt=$coordinates d=$distance}";
 
}
?>

I then search for "ma", in Drupal this time (!), and my results reflected the nodes within the Boston area.

Drupal Solr GeoSpatial Results

In this article, I'll explain one way to embed a GMAP view into your search results.

I downloaded and installed the following modules:

content (cck)
location_cck
gmap
gmap_location
location
location_search
views
views_ui

Configuring the GMap and Location modules:

  • Location main settings (admin/settings/location), ensure "Enable JIT geocoding" is enabled.
  • Location map links (admin/settings/location/maplinking), ensure "Google Maps" is enabled for Unites States.
  • Location Geocoding options (admin/settings/location/geocoding), enable "Google Maps" for United States.
  • GMap (admin/settings/gmap), enter your Google Maps API key.

I then imported US zip codes into my Drupal database:

$ cd sites/all/modules/contrib/location
$ mysql db_drupal < zipcodes.us.mysql

I created a content type with a single Location CCK field for address. I made this field required and set my desired collection settings. I added some sample nodes, and verified the location coordinates were being added dynamically.

Next, I added a new view with the following settings:

  • Name: search_gmap
  • Type: Node
  • Style: GMap; macro: [gmap behavior=+autozoom]; Data source: Location.module;
  • Fields: Node: Title
  • Filters: Node: Published
  • Arguments: Node: Nid; Action to take if argument is not present: display empty text; Enabled: Allow multiple terms per argument.

Lastly, I added some code to my module:

<?php
function MYMODULE_preprocess_search_results(&$vars) {
 
 
// get nodeids from search results
 
$node_ids = array();
  foreach (
$vars['results'] as $result) {
    if (!
is_object($result['node'])) {
      continue;
    }
   
$node_ids[] = $result['node']->nid;
  }

 
// ensure node ids exist
 
if (!count($node_ids)) {
    return;
  }

 
// generate views output
 
$view_output = views_embed_view('search_gmap', 'default', implode('+', $node_ids));
  if (
$view_output) {   
   
$vars['search_results'] = $view_output . $vars['search_results'];
  }

}
?>

After flushing my caches, and running cron to index my nodes, I ran a search for "nashua":

GMap search results

In this article, I'll show an example of how to implement a Subversion pre-commit hook to integrate with Drupal. Pre-commit hooks can be used to execute any arbitrary code, such as deployment procedures, archiving databases, etc. For this example, I will show how to check for the creation of a subversion tag and archive the database.

To get started, I created a local subversion repository.

# create folder for subversion repositories
$ mkdir /var/subversion

# create the subversion repository
$ svnadmin create /var/subversion/project

Upon creating a new local svn repository, a hooks directory will be created. Example: /var/subversion/project/hooks

Inside this directory will be a bunch of sample scripts ending in ".tmpl" which contain example hook scripts. Here are the contents of the example pre-commit hook without comments:

$ cat pre-commit.tmpl | egrep -iv "(^#|^$)"
REPOS="$1"
TXN="$2"
SVNLOOK=/usr/bin/svnlook
$SVNLOOK log -t "$TXN" "$REPOS" | \
   grep "[a-zA-Z0-9]" > /dev/null || exit 1
commit-access-control.pl "$REPOS" "$TXN" commit-access-control.cfg || exit 1
exit 0

As noted in the pre-commit.tmpl file, there are 2 arguments being passed to the pre-commit script:

[1] REPOS-PATH   (the path to this repository)
[2] TXN-NAME     (the name of the txn about to be committed)

I created a new file called "pre-commit" and added the following contents:

#!/bin/bash

/var/www/vhosts/project.vm/scripts/svn-pre-commit.php "$1" "$2"

I then made the file executable.

$ chmod ug+w pre-commit

The above script simply passes the arguments to a PHP script contained with the Drupal project.

In my scripts folder (/var/www/vhosts/project.vm/scripts), I created the PHP script "svn-pre-commit.php" with the following contents:

#!/usr/bin/php
<?php

// get args
$repo = $argv[1];

// define path to mysql backups
$mysql_backups_path = '/var/www/vhosts/project.vm/database';

// define path to drupal docroot
$drupal_docroot_path = '/var/www/vhosts/project.vm/htdocs';

// get changed path
// example output:
// A   tags/20110503/
$svn_look = `svnlook changed $repo`;

// define pattern to break apart svnlook changed
$pattern = '/^\s*([A-Za-z])\s*(.*)$/';

// execute preg match
preg_match($pattern, $svn_look, $matches);
$svn_action = $matches[1];
$svn_changed_path = $matches[2];

// check if a tag is being created
if ($svn_action == 'A' && substr($svn_changed_path, 0, 5)=='tags/') {

  // get tag name
  $exploded = explode('/', $svn_changed_path);
  $tag_name = $exploded[1];
 
  // change dir to drupal docroot
  chdir($drupal_docroot_path);

  // backup mysql database using drush
  `/var/www/drush/drush sql-dump > {$mysql_backups_path}/tag_{$tag_name}.sql`;

}

I also made this script executable:

$ chmod ug+w svn-pre-commit.php

Now, assuming that my Drupal site is integrated with the subversion repository, and development is at a point to deploy/create a new tag, I executed the following command to create the subversion tag:

To verify, I entered the directory containing my database dumps to checkout the result:

$ cd /var/www/vhosts/project.vm/database

$ ls -1
tag_beta-0.1.sql

In this article, I'll show the commands I have been using to set up a fresh Centos server, configured for Apache, MySQL, PHP, Tomcat, Drupal, and Apache Solr. For my article, I used Parallels to create a virtual machine from the Centos 5.6 64bit ISOs I downloaded. To simply this article, all commands are being executed as root, firewall configurations and performance tweaks are not accounted for.

Once the distribution is installed, the first thing I do is upgrade all packages.

$ yum update

Install PHP, Apache, and MySQL

$ yum install php53 php53-gd php53-mbstring php53-mysql php53-xml mysql-server httpd

Set runlevels for Apache and MySQL

$ chkconfig --level 2345 httpd on
$ chkconfig --level 2345 mysqld on

Install subversion. I chose to use subversion for this article because the Drupal 6.x installation works well with svn:externals to fetch the SolrPhpClient library. All subversion commands are connecting to a local subversion repository. If you are using an external server (like Beanstalk), you will have to transpose all commands from using "file://" to "https://".

$ yum install subversion

Add a new local subversion repository (OPTIONAL).

$ mkdir /var/subversion
$ svnadmin create /var/subversion/example.com
$ svn mkdir file:///var/subversion/example.com/trunk -m "added trunk"
$ svn mkdir file:///var/subversion/example.com/branches -m "added branches"
$ svn mkdir file:///var/subversion/example.com/tags -m "added tags"

Download/setup drush

$ cd /var/www
$ wget http://ftp.drupal.org/files/projects/drush-7.x-4.4.tar.gz
$ tar -xzf drush-7.x-4.4.tar.gz
$ ln -s /var/www/drush/drush /usr/local/bin/drush

Create a vhost location on the server for the Drupal installation.

$ mkdir /var/www/vhosts
$ cd /var/www/vhosts
$ drush dl drupal
$ mv drupal-7.0/ example.com

Integrate the Drupal files with subversion

$ cd /var/www/vhosts/example.com
$ svn co file:///var/subversion/example.com/trunk .
$ svn add * .htaccess
$ svn commit -m "downloaded drupal"

Download the Drupal apachesolr module

# make a folder for contrib modules
$ mkdir /var/www/vhosts/example.com/sites/all/modules/contrib
$ cd /var/www/vhosts/example.com/sites/all/modules/contrib

# note: in the below command, you may be prompted to choose which version of the Solr module to install. I choose option 2 for the Supported version
$ drush dl apachesolr

# commit to subversion
$ cd /var/www/vhosts/example.com/sites/all/modules
$ svn add contrib
$ svn commit -m "added contrib folder and apachesolr module"

Setup MySQL

# start mysql
$ /etc/init.d/mysqld start

# set root password
$ /usr/bin/mysqladmin -u root password 'new-password'

# create new database, user, and set permissions
$ mysql --execute="create database db_example"
$ mysql --execute="grant all privileges on db_example.* to 'example-user'@'localhost' identified by 'some_password'"

Setup Apache vhost

$ cd /etc/httpd/conf.d

# create a new file "example.com.conf", with the contents:

NameVirtualHost *:80

<Directory /var/www/vhosts>
  AllowOverride All
</Directory>

<VirtualHost *:80>
  ServerName example.com
  DocumentRoot /var/www/vhosts/example.com
  ErrorLog logs/example.com-error_log
  CustomLog logs/example.com-access_log common
</VirtualHost>

Reset Apache file permissions. NOTE: you will need a more solid/secure configuration for this!

$ cd /var/www
$ chown -R apache.apache drush*
$ chown -R apache.apache vhosts

Start Apache

$ /etc/init.d/httpd start

Install Drupal via drush

$ cd /var/www/vhosts/example.com

# note: you can set your user 1 username, password, email, etc in the following command if desired. type "drush help si" for more install options
$ drush site-install standard --sites-subdir=example.com --db-url=mysqli://example-user:some_password@localhost/db_example

At this point, you should be able to browse to your site and it will be up and running.
Drupal Installed

Now, we move onto Tomcat and Solr!

Installing Tomcat and Java. The default Centos yum repositories provide Tomcat5. I prefer Tomcat6, so there are some extras steps below and a dependency issue I had to resolve.

# added repo file to get tomcat6:
$ cd /etc/yum.repos.d/
$ wget http://www.jpackage.org/jpackage50.repo

# install Java JDK:
$ yum install java-1.6.0-openjdk

# install tomcat6:
$ yum install tomcat6 tomcat6-admin-webapps tomcat6-webapps

# dang, dependency issue... (!)

java-1.4.2-gcj-compat-1.4.2.0-40jpp.115.x86_64 from base has depsolving problems
  --> Missing Dependency: /usr/bin/rebuild-security-providers is needed by package java-1.4.2-gcj-compat-1.4.2.0-40jpp.115.x86_64 (base)
Error: Missing Dependency: /usr/bin/rebuild-security-providers is needed by package java-1.4.2-gcj-compat-1.4.2.0-40jpp.115.x86_64 (base)
You could try using --skip-broken to work around the problem
You could try running: package-cleanup --problems
                        package-cleanup --dupes
                        rpm -Va --nofiles --nodigest
The program package-cleanup is found in the yum-utils package.

# Fixing dependency issue (OPTIONAL):
$ mkdir ~/downloads
$ cd ~/downloads
$ wget http://plone.lucidsolutions.co.nz/linux/centos/images/jpackage-utils-com...
$ rpm -Uvh jpackage-utils-compat-el5-0.0.1-1.noarch.rpm
$ yum install tomcat6 tomcat6-admin-webapps tomcat6-webapps

# setting tomcat runlevels
$ chkconfig --level 2345 tomcat6 on

# starting tomcat
$ /etc/init.d/tomcat6 start

At this point, you should be able to access Tomcat in your browser (http://example.com:8080)
Tomcat homepage

Downloading Solr Java library.

$ cd ~/downloads
# note: you may need to choose a different mirror to download
$ wget http://www.fightrice.com/mirrors/apache//lucene/solr/1.4.1/apache-solr-1...
$ tar -xzf apache-solr-1.4.1.tgz

# copy/rename solr war file into Tomcat webapps directory
$ cp ~/downloads/apache-solr-1.4.1/dist/apache-solr-1.4.1.war /var/lib/tomcat6/webapps/solr.war

# copy solr files
$ cp -r ~/downloads/apache-solr-1.4.1/example/solr/ /var/lib/tomcat6/solr/

Copying the java war file into the Tomcat webapps folder will create this directory automatically:

/var/lib/tomcat6/webapps/solr

Create Catalina config file to link war file to solr directory:

$ cd /etc/tomcat6/Catalina/localhost

# create new file: "solr.xml", with the contents:

<?xml version="1.0" encoding="UTF-8"?>
<Context docBase="/var/lib/tomcat6/webapps/solr.war" debug="0" privileged="true" allowLinking="true" crossContext="true">
<Environment name="solr/home" type="java.lang.String" value="/var/lib/tomcat6/solr" override="true" />
</Context>

Setup Tomcat admin user(s):

# edit file: /etc/tomcat6/tomcat-users.xml, ensure similar contents exist:

<?xml version='1.0' encoding='utf-8'?>
<tomcat-users>
<role rolename="admin"/>
<role rolename="manager"/>
<user username="eric" password="supersecretpassword" roles="admin,manager"/>
</tomcat-users>

Update WEB-INF/web.xml file:

# edit file: /var/lib/tomcat6/webapps/solr/WEB-INF/web.xml, update section to reflect solr path:

<env-entry>
  <env-entry-name>solr/home</env-entry-name>
  <env-entry-value>/var/lib/tomcat6/solr</env-entry-value>
  <env-entry-type>java.lang.String</env-entry-type>
</env-entry>

Copy conf files from Drupal apachesolr module into Tomcat Solr conf directory (overwrite):

$ cp /var/www/vhosts/example.com/sites/all/modules/contrib/apachesolr/protwords.txt /var/lib/tomcat6/solr/conf/
$ cp /var/www/vhosts/example.com/sites/all/modules/contrib/apachesolr/schema.xml /var/lib/tomcat6/solr/conf/
$ cp /var/www/vhosts/example.com/sites/all/modules/contrib/apachesolr/solrconfig.xml /var/lib/tomcat6/solr/conf/

Reset Tomcat permissions/ownership:

$ cd /var/lib
$ chown -R tomcat.tomcat tomcat6/

Restart Tomcat

$ /etc/init.d/tomcat6 restart

At this point, you should be able to access the solr/admin tomcat Page (http://example.com:8080/solr/admin)
Solr Admin

If things are not working well at this point, check the Tomcat logs:

/var/log/tomcat6/catalina.out

And, ensure the solr java module is listed in the Tomcat Web Application Manager: http://example.com:8080/manager/html

If all is well, you can now enable the Drupal apachesolr modules:

$ cd /var/www/vhosts/example.com
$ drush en apachesolr apachesolr_search apachesolr_taxonomy apachesolr_access --uri=example.com

Log into your Drupal site. NOTE: default account (via drush): admin/admin

Edit default Apache Solr Host Settings:
URL: http://example.com/admin/config/search/apachesolr/server/solr/edit
Change url to: http://example.com:8080/solr, and save form.

Go to Drupal search settings page:
URL: http://example.com/admin/config/search/settings
Change the default search mode to "Apache Solr search", and save form.

Now, you are ready to test the indexing and integration.

Add a new piece of content to test indexing.
# Example:
# http://example.com/node/add/article
# title: Test Article
# Body: test test test

Browse to solr index page:
URL: http://example.com/admin/config/search/apachesolr/index
Select: Index queued content, and click on Begin button
You should see a status message like: "1 item processed successfully." and "Number of documents in index: 0 (1 sent but not yet processed)"
A few minutes later, refreshing the index page should show: "Number of documents in index: 1"

Search for "test" to verify Solr results.
URL: http://example.com/search/site/test
Solr Search Results

You can also review search results via solr/admin
URL: http://example.com:8080/solr/admin/
Enter "test" in query string box and click search

Part 2, Multicore Configuration (OPTIONAL)

If you need to run multiple sites off a single Solr Tomcat installation, you can setup multicore..

Copy the multicore xml file into your solr directory:

$ cp ~/downloads/apache-solr-1.4.1/example/multicore/solr.xml /var/lib/tomcat6/solr/

Create a new directory for each multisite in the solr directory:

$ mkdir /var/lib/tomcat6/solr/example.com

Replicate the solr conf directory into the new multisite directory:

cp -r /var/lib/tomcat6/solr/conf /var/lib/tomcat6/solr/example.com/conf/

Update the solr.xml file:

# edit file: /var/lib/tomcat6/solr/solr.xml, added <core> section for each site:
<cores adminPath="/admin/cores">
  <core name="example.com" instanceDir="example.com" />
</cores>

Restart Tomcat

$ /etc/init.d/tomcat6 restart

Now, your new multicore site will be accessible here: http://example.com:8080/solr/example.com/admin/