background image
HomeRecent PostsDrupalSearchTagsRSSContactAboutAccount
Eric.London's picture

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

Recently I was working on a project that had a view with an exposed filter for country (via location module) and the dropdown list by default showed the entire country list, which was very large. I decided to write a code snippet that would reduce the select options to show only the countries that have been assigned to the nodes.

In my sandbox, I create a simple node type for "person" that used the location module to capture the user's country. I then created a view to show the people's names and countries, and added an exposed filter for country.

People & Countries View

Clicking on the exposed filter showed all of the available countries, which was a very large list.

Country dropdown

I added the following code to my module to alter the exposed filter form and reduce the country list. (Please note, an alternative approach would be to modify the views object, I wanted to show how this could be accomplished in hook_form_alter())

<?php
/**
* Implements hook_form_alter()
*/
function MYMODULE_form_alter(&$form, $form_state, $form_id) {

 
// define variables
 
$view_name = 'people_places';
 
$filer_name = 'country';

 
// modify country drop down
 
if ($form_id == 'views_exposed_form' && is_object($form_state['view']) && $form_state['view']->name==$view_name && is_array($form[$filer_name])) {
        
   
// define SQL to fetch nodes that have countries
   
$sql = "
      select distinct
        l.country
      from
        {node} n
      join
        {location_instance} li on li.nid = n.nid and li.vid = n.vid
      join
        {location} l on l.lid = li.lid
      where
       n.status = 1 and n.type = 'person'
    "
;
  
   
// fetch results
   
$resource = db_query($sql, $tid);
   
$countries = array();
    while(
$row = db_fetch_object($resource)) {
     
$countries[] = $row->country;
    }
  
   
// filter country list
   
foreach($form[$filer_name]['#options'] as $key => $value) {
    
     
// allow "All"
     
if ($key == 'All') {
        continue;
      }
    
      elseif (!
in_array($key, $countries)) {
        unset(
$form[$filer_name]['#options'][$key]);
      }
    
    }
  
  }
}
?>

Now the view only shows countries that match the resulting nodes.

Filtered exposed filter

In this blog entry I'll show how you can create a Google map showing where your users are located using CCK, Content Profile, Views, GMap, and Location modules. Once you've downloaded and extracted those modules, enable the following modules (and set permissions accordingly):

Content
Location
Location CCK
Content Profile
GMap
GMap Location
Views
Views UI

Next you'll need to configure these new modules. On the GMap settings page (admin/settings/gmap) you'll need to add your API Key and enable the setting to Use AutoZoom. On the Location settings page (admin/settings/location) enable the checkbox to use a Google Map to set latitude and longitude and enable JIT geocoding. On the geocoding settings page (admin/settings/location/geocoding), you'll have to enable Google Maps for the desired countries.

The Content Profile module creates a new content type called "Profile". You'll need to add a new field to this content type for the user's location (admin/content/node-type/profile/fields). Enter a label for the node type (Location), enter a field name (field_location), choose "Location" for the field type, and choose "Location Field" for the widget.

On the next screen edit the Locative information sections (Collection and Display) to your liking, and save the field settings. For my example, I enabled Street location, Additional, City, State, Postal code, Country, and Coordinate Chooser.

Now if you create your profile node (node/add/profile) you can enter your location information and save the new node. If all is working at this point, if you return to the profile node edit screen and scroll down to the location information, you should now see a marker on map for your location and the geocoded information will be shown.

Now you can add a new view (admin/build/views/add) to show the user's profile locations. Enter a name for your view, choose node for the view type, and click the next button. Add a new filter for "Node: Published" is true and "Node: Type" is one of "Profile". Add a new relationship for Content: Location (field_location). Add fields for "Location: Latitude" (using the Location relationship), "Location: Longitude" (using the Location relationship), and any additional fields you'd like to display in the GMap marker (title, parts of the address, etc). For the Style of the view choose "GMap". For the GMap style settings, select "Choose latitude and longitude fields" for the Data Source and ensure the Latitude and Longitude fields as set.

Lastly, I added a page view so I could see the results in action..

ps> special thanks to everyone at CommonPlaces for their great technical discussions about these modules!

Syndicate content