Geospatial Search using Solr in grails

Posted By : Jasgeet Singh | 10-Jun-2015

Suppose we have a search application that is storing information about the companies. Every company is described by a name and two floating point numbers that represent the geographical location of the company. One day your boss comes to your room and says that he/she wants the search results to be sorted by distance from the user's location. This recipe will show you how to do it.

1. Let's begin with the following index (add the following to your schema.xml file to the fields section): 

<field indexed="true" name="id" required="true" stored="true" type="string"/>
<field indexed="true" name="name" stored="true" type="text"/>
<field indexed="true" name="location" stored="true" type="location"/>
<dynamicfield indexed="true" name="*_coordinate" stored="false" type="tdouble"/>

2. We also have the following type defined in the schema.xml file:

<fieldtype class="solr.LatLonType" name="location" subfieldsuffix="_coordinate"/>

I assumed that the user location will be provided from the application that is making a query. Now index your data file

def latLng = "54.7890" +","+ "78.45678"
doc.addField( store, latLng)
doc.addField( "lat_coordinate",54.7890)
doc.addField( "lng_coordinate",78.45678)

 

Constitute SolrQuery for a haversine based spatial search. This method returns the SolrQuery object in case you need to manipulate it further (add facets, etc)

    /**
	 * @param query  a lucene formatted query to execute in addition to the location range query
	 * @param lat    latitude in degrees
	 * @param lng    longitude in degrees
	 * @param range  the proximity range to filter results by (small the better performance). unit is miles unless the       *  radius param is passed in which case it's whatever the unit of the radius is
	 * @param start  result number of the first returned result - used in paging (optional, default: 0)
	 * @param rows   number of results to include - aka page size (optional, default: 10)
	 * @param sort   sort direction asc or desc (optional, default: asc)
	 * @param funcQuery provide a function query to be summed with the hsin function (optional)
	 * @param radius sphere radius for haversine algorigthm (optional, default: 3963.205 [earth radius in miles])
	 * @param lat_field SOLR index field for latitude in radians (optional, default: latitude_d)
	 * @param lng_field SOLR index field for latitude in radians (optional, default: longitude_d)
	 * @return SolrQuery object representing this spatial query
	 */
	
      SolrQuery getSpatialQuery(query, lat, lng, range, start=0, rows=10, sort="asc", funcQuery="", radius=3963.205, lat_field="latitude_rad_d", lng_field="longitude_rad_d") {
		def lat_rad = Math.toRadians( lat )
		def lng_rad = Math.toRadians( lng )
		def hsin = "hsin(${lat_rad},${lng_rad},${lat_field},${lng_field},${radius})"
		def order = [asc: SolrQuery.ORDER.asc, desc: SolrQuery.ORDER.desc]
		if(funcQuery != "")
			funcQuery = "${funcQuery},"
		SolrQuery solrQuery = new SolrQuery( (query) ? "(${query}) AND _val_:\"sum(${funcQuery}${hsin})\"" : "_val_:\"sum(${funcQuery}${hsin})\"" )
		solrQuery.addFilterQuery("{!frange l=0 u=${range}}${hsin}")
		solrQuery.setStart(start)
		solrQuery.setRows(rows)
		solrQuery.addSort("score", order[sort])
		return solrQuery
	}

	/**
	 * Expected to be called after getSpatialQuery, assuming you need to further
	 * manipulate the SolrQuery object before executing the query.
	 * @return Map with 'resultList' - list of Maps representing results and 'queryResponse' - Solrj query result
	 */
	
     def querySpatial(SolrQuery solrQuery, lat, lng, lat_field="latitude_rad_d", lng_field="longitude_rad_d") {
		log.debug ("spatial query: ${solrQuery}")
		def result = search(solrQuery)
		result.getResults().each {
			try{
				println "str -- -  > "+SolrUtil.stripFieldName(lat_field)
			}catch(Exception e){
				println "e-------> "+e
			}
			it.dist = Haversine.computeMi(lat, lng, Math.toDegrees(it."${SolrUtil.stripFieldName(lat_field)}"), Math.toDegrees(it."${SolrUtil.stripFieldName(lng_field)}"))
		}
		return result
	}

Some Other Useful Spatial Query Parameters are:
a) geofilt - The distance filter

b) geodist - The distance function

/**
	 * This method returns all results within a circle of the given radius around the given user coordinates point.
	 * @param user
	 * radius - the radial distance, in kilometers
	 * @return
	 */
	def getSpatialSearch(User user, int radius = 5, double lat, double lng){
		try {
			def solrQuery = new SolrQuery("*")
			solrQuery.addFilterQuery("{!geofilt}")
			solrQuery.setParam("sfield","store")
			solrQuery.setParam("d","${radius}")
			solrQuery.addField("id")
			solrQuery.addField("store")
			solrQuery.addField("score")
			solrQuery.setParam("pt","${lat},${lng}")
			solrQuery.addField("geodist()")
			solrQuery.setSort("geodist()", SolrQuery.ORDER.asc)
			solrQuery.addFilterQuery(/+type:com.threebaysover.security.User/)
			return getServer().query( solrQuery, SolrRequest.METHOD.POST )
		}catch(Exception exception){
			log.debug("Exception Occurs while doing spatial search for user "+user+" "+exception.message)
		}
	}

Hope you liked this content and can implement someway to your requirements, see you soon!

About Author

Author Image
Jasgeet Singh

Jasgeet is a Sr. Lead developer .He is an experienced Groovy and Grails and has worked on designing & developing B2B and B2C portals using Grails technologies.

Request for Proposal

Name is required

Comment is required

Sending message..