Tuesday, May 7, 2013

2dsphere Indexes in Mongo 2.4

MongoDB 2.4 has added  new spherical index, 2dsphere.  This index allows for more precise geo-spatial querying within Mongo by taking into the account the spherical shape of the earth and the fact that the distances between lines of longitude shrink and grow, depending on the latitude.  Here is a good link to understand the Earth's spherical nature:  http://www.learner.org/jnorth/tm/LongitudeIntro.html

A simple application of this 2dsphere index is seen below.  Given the "locations" collection with the given "typical" document shape:

/* 0 */
{
  "_id" : ObjectId("51887218c0aa488a0394002f"),
  "_class" : "com.icfi.mongo.data.model.Location",
  "city" : "Wheeling",
  "state" : "WV",
  "coords" : [40.071472, -80.6868],
  "timeZone" : -5,
  "zipCode" : "26003",
  "dstObserved" : true
}
I will apply a 2dsphere index on the "coords" (short for coordinates) element.  This element is an array of double precision numbers representing the longitude and latitude (in that order) of a given city.  The command is below:
db.locations.ensureIndex( { "coords" : "2dsphere" } )
Next we can run query using a geo-spatial operator, like $near.  The $near command syntax is seen below:


db.collection<collection>.find( { <location field=""> :
                         { $near :
                            { $geometry :
                                { type : "Point" ,
                                  coordinates : [ <longitude> , <latitude> ] } },
                              $maxDistance : <distance in="" meters="">
                      } } )

My actual command to search for cities near Wheeling, WV 26003 is:

db.locations.find({ 'coords' : { $near : { $geometry : { type : 'Point' ,coordinates : [ 40.071472 , -80.6868 ] } }, $maxDistance : 10000} })
This search returns documents that are within a circular distance of 10000 meters for the given Long/Lat coordinates.


To make this more friendly in Java, I added the utility to convert miles to meters; I don't use meters much.

public class GeoUtils {

 public static final double MILES_METERS_DIVISOR = 0.00062137;

 public static double milesToMeters(double miles) {
  return miles / GeoUtils.MILES_METERS_DIVISOR;
 }
}

A JUnit test is seen below.  First I get the location object from which I want to harvest the coordinates (point).  Then I call the LocationService method to find the nearest cities.  I have also include the Location model class and the Spring Data LocationRepository class.


@Test
 public void testNearMiles() {
  log.info("<<<<<<<<<<<<<<<<<  testNearMiles  >>>>>>>>>>>>>>>>>>>>");

  List<Location> locations = locationService.findByCityAndState(
    "Wheeling", "WV");

  assertNotNull("locations[0] was null.", locations.get(0));
  
  assertEquals("City was not correct.", "Wheeling", locations.get(0)
    .getCity());
  assertEquals("State was not correct.", "WV", locations.get(0)
    .getState());
  assertEquals("ZipCode was not correct.", "26003", locations.get(0)
    .getZipCode());

  List<Location> locales = this.locationService.findNear(
    locations.get(0), 5);

  for (Location locale : locales) {
   log.info(locale.toString());
  }
  
  assertEquals("City was not correct.","Yorkville",locales.get(2).getCity());
  assertEquals("City was not correct.","Glen Dale",locales.get(14).getCity());
 }

Location service class:

...
@Override
 public List<Location> findNear(double lon, double lat, double distance) {
  return this.locationRepository.findByGeoNear(lon, lat, distance);
 }

 @Override
 public List<Location> findNear(Location location, double distanceInMiles) {
  return this.findNear(location.getLongitude(), location.getLatitude(),
    GeoUtils.milesToMeters(distanceInMiles));
 }
...

In the LocationRepository class I have the following annotated method:

...
@Query("{ 'coords' : { $near : { $geometry : { type : 'Point' ,coordinates : [ ?0 , ?1 ] } }, $maxDistance : ?2} }")
List<Location> findByGeoNear(double lon, double lat, double distance);
Since I am using Spring Data, I can add a new method to my LocationService like so:


@Override
 public GeoResults<Location> findNearPoint(Location location,
   double distanceInMiles) {

  Point point = new Point(location.getLongitude(), location.getLatitude());

  NearQuery query = NearQuery.near(point).maxDistance(
    new Distance(distanceInMiles, Metrics.MILES));

  GeoResults<Location> results = this.mongoOps.geoNear(query,
    Location.class);

  return results;
 }
This new method uses a new Point class and the Distance class that handles the miles appropriately.  This approach is considered a "GEO Near" query.  It finds the locations near the given point and calculates the actual distance from the original point to each resultant location.  Results are returned in a parameterized GeoResults object.

No comments:

Post a Comment