Geospatial Queries with Google Cloud Datastore (Java)

NOTICE
This article describes geospatial queries for Google Cloud Datastore + Java.
Please keep in mind that using Google Cloud Platform you can also use Cloud SQL (compatible with MySQL), which allows to use much more solutions / libraries for geospatial queries.


Geospatial queries are integral part of modern applications which allow to serve geo-contextual data -  ex. shops in the nearest surroundings.

PROBLEM: Currently Google Cloud Datastore doesn't provide built-in API to execute geospatial queries

This functionality is under development (alpha version, invitation only): https://cloud.google.com./appengine/docs/java/datastore/geosearch
Datastore Geosearch API is available on local environment (DEVSERVER), but if you don't have invitation to use alpha version, you cannot deploy your solution to production:

Creating an index failed for entity_type: "Location"ancestor: falseProperty {  name: "geoPt"  mode: 3}: Permission denied for creating a search index

CURRENT SOLUTION: JavaGeoModel

For the time Datastore doesn't support native geospatial queries, you can use JavaGeoModel library ( https://github.com/apigee/javageomodel) in combination with JPA (or with any other API after implementing QueryEngine interface)

Step 1: Annotate your domain class

Annotate your domain class to distinguish fields which store latitude and longitude and special field to keep information about geocells.

package co.quickmenu;

import com.beoui.geocell.annotations.Geocells;
import com.beoui.geocell.annotations.Latitude;
import com.beoui.geocell.annotations.Longitude;


public class Location {

 private String name;

 @Latitude
 private double latitude;
 
 @Longitude
 private double longitude;

 @Geocells
 private List<String> geoCells;

 //not listing getters/setters to keep this example short
 
}

Step 2: Store Latitude / Longitude and Geocells data for every object

Ensure that every object in your DB has information about latitude, longitude and geoCells.

GeoCells can be generated using GeocellManager:
import com.beoui.geocell.GeocellManager;
import com.beoui.geocell.model.


//create object and setup geo location
Location location = new Location(lat, lng);
List<String> geoCells = 
    GeocellManager.generateGeoCell(new Point(lat, lng));
location.setGeoCells(geoCells);

//store object using your persistence API
...

Step 3: Implement method to search objects within given circle (point + radius)

JPA API queries

If you are using JPA, just pass EntityManager to GeocellManager together with center point and radius. If you're using non-JPA method, please navigate to "Non-JPA queries" section.

private List<Location> findLocations(Double lat, Double lng, 
                                     Double maxDistanceInMeters) {
    Point center = new Point(lat, lng);
    EntityManager em = 
            EntityManagerFactoryCache.getInstance().createEntityManager();
    //Additional conditions can be applied using base query
    GeocellQuery activeLocQuery = 
            new GeocellQuery("select location from Location location " + 
                             "where location.active=true");

    List<Location> result = GeocellManager.proximitySearch
                              (center, MAX_RESULTS, maxDistanceInMeters, 
                                           Location.class, baseQuery, em);
    em.close();
    return result;
        
}

NOTICE: JPA implementation of GeocellQueryEngine (com.beoui.geocell.JPAGeocellQueryEngine) doesn't support result limit (parameter passed to  GeoCellManager.proximitySearch won't be applied). It's design issue and may lead to memory management issues.

Please keep in mind that JPA is not recommended for use with Datastore (https://cloud.google.com/appengine/docs/java/datastore/jpa/overview)

 

Non-JPA API queries

If you don't use JPA, you should implement class extending GeocellQueryEngine interface (specific for your persistence API), and pass instance of this class implementation to GeoCellManager.proximitySearch:

package com.beoui.geocell;

import java.util.List;
import com.beoui.geocell.model.GeocellQuery;

public interface GeocellQueryEngine {
    public <T> List<T> query(GeocellQuery baseQuery, 
        List<String> curGeocellsUnique, Class<T> entityClass);

}


//instantiate your implementation of query engine
GeocellQueryEngine queryEngine = new DatastoreQueryEngine();

//pass it as an argument to GeocellManager#proximitySearch
List<Location> locationsList = GeocellManager.proximitySearch(center,
   MAX_RESULTS, 0, maxDistanceInMeters, Location.class, baseQuery,
   queryEngine, GeocellManager.MAX_GEOCELL_RESOLUTION)
   .getResults();

EXAMPLE: Simple implementation for Datastore API (notice: it doesn't support base query).

package co.quickmenu;

import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;

import com.beoui.geocell.GeocellQueryEngine;
import com.beoui.geocell.GeocellUtils;
import com.beoui.geocell.model.GeocellQuery;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.Query;

/**
 * Simple implementation for Datastore (simple == ignores baseQuery)
 * 
 */
public class DatastoreQueryEngine implements GeocellQueryEngine {
    
    private static Logger logger = 
        Logger.getLogger(DatastoreQueryEngine.class.getName());
    
    private static int LIMIT = 200;

    /* (non-Javadoc)
     * @see com.beoui.geocell.GeocellQueryEngine#
     * query(com.beoui.geocell.model.GeocellQuery, 
     * java.util.List, java.lang.Class)
     */
    @Override
    public <T> List<T> query(GeocellQuery baseQuery,
            List<String> curGeocellsUnique, Class<T> entityClass) {
        Object geocellsFieldName = 
            GeocellUtils.getGeocellsFieldName(entityClass);
        logger.info("Executing query: " + 
            entityClass.getSimpleName() + "." + geocellsFieldName + 
            " in " + curGeocellsUnique);
        DatastoreService datastore = 
            DatastoreServiceFactory.getDatastoreService();
        Query query = new Query(entityClass.getSimpleName());
        Query.FilterPredicate geoCellsFilter = new Query.FilterPredicate
                ("geoCells", FilterOperator.IN, curGeocellsUnique);
        query.setFilter(geoCellsFilter);
        List<Entity> entities = datastore.prepare(query)
            .asList(FetchOptions.Builder.withLimit(LIMIT));
        logger.info("Got " + entities.size() + " " + 
            entityClass.getSimpleName() + " matching criteria.");
        return (List<T>)entities;
    }
    
}

SUMMARY

Cons and pros of JavaGeoModel library:

PROSCONS
  • Open source
  • Easy to use
  • Extensible
  • Sufficient performance
  • Not very mature
  • No active development / support


Other solutions

Google Search API:
http://ralphbarbagallo.com/2013/05/14/app-engine-geospatial-datastore-search-a-new-way/





Komentarze

Popularne posty z tego bloga

How to start developing Google App Engine applications

How to deploy Ruby on Rails application on Google Compute Engine