How to Set Up Geolocation Search in Your App with Elasticsearch

Location-based features have become ubiquitous in modern applications. Whether you‘re building a store locator, a ride-sharing app, or a social network, adding geolocation capabilities can greatly enhance your app‘s functionality and user experience.

While implementing geolocation search might seem daunting at first, Elasticsearch makes it relatively straightforward. As a distributed search and analytics engine, Elasticsearch provides a powerful suite of tools for indexing and querying geospatial data.

In this in-depth guide, we‘ll walk through the process of setting up geolocation search with Elasticsearch from scratch. We‘ll cover everything from installing Elasticsearch to indexing and querying geospatial data to implementing a location-based search feature in a sample Java application. Let‘s get started!

Setting Up an Elasticsearch Cluster

The first step is to set up an Elasticsearch cluster. You can install Elasticsearch on your own servers or use a hosted solution like Elastic Cloud. For local development and testing, you can also run Elasticsearch in Docker containers.

To install Elasticsearch, follow the instructions for your operating system in the official documentation. Make sure to install a version that is compatible with the Elasticsearch client libraries you‘ll be using in your app.

Once you have Elasticsearch up and running, you can verify the installation by sending a GET request to the root endpoint:

curl http://localhost:9200/

You should see a response similar to this:

{
"name" : "node-1",
"cluster_name" : "my-cluster",
"cluster_uuid" : "xxxxxxxxxx",
"version" : {
"number" : "7.12.0",
"build_flavor" : "default",
"build_type" : "tar",
"build_hash" : "xxxxxxxxxx",
"build_date" : "yyyy-mm-ddTHH:MM:SS.sssZ",
"build_snapshot" : false,
"lucene_version" : "8.8.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}

Creating an Index with Geo Mapping

In Elasticsearch, related data is stored in an index (analogous to a database in the relational DB world). Before we can index any geospatial data, we need to create an index with the appropriate mapping.

Suppose we want to build a store locator app that allows users to search for nearby coffee shops. Let‘s create an index called "coffee-shops" with a geo_point field for storing location coordinates:

PUT /coffee-shops
{
"mappings": {
"properties": {
"name": { "type": "text" },
"location": { "type": "geo_point" }
}
}
}

Here we defined two fields – "name" for the coffee shop name and "location" for its geographical coordinates. The "location" field is mapped as a geo_point, which tells Elasticsearch to treat the value as a latitude/longitude pair.

Indexing Geospatial Data

Now that we have our "coffee-shops" index, let‘s add some documents with geospatial data:

POST /coffee-shops/_doc/1
{
"name": "Coffee Shop A",
"location": {
"lat": 40.715681,
"lon": -74.009432
}
}

POST /coffee-shops/_doc/2
{
"name": "Coffee Shop B",
"location": "40.709186, -74.013012"
}

POST /coffee-shops/_doc/3
{
"name": "Coffee Shop C",
"location": {
"lat": 40.721612,
"lon": -73.999985
}
}

In the examples above, we indexed three coffee shops with their names and locations. Notice that the "location" field accepts both an object with "lat" and "lon" keys as well as a string in the "latitude,longitude" format.

Querying Geospatial Data

Elasticsearch provides several geo query types for searching geospatial data:

Geo Bounding Box Query

The geo_bounding_box query finds documents with geo-points that fall within a rectangular bounding box. For example, to search for coffee shops within a box spanning from 40.70 to 40.74 latitude and -74.02 to -73.98 longitude:

GET /coffee-shops/_search
{
"query": {
"geo_bounding_box": {
"location": {
"top_left": {
"lat": 40.74,
"lon": -74.02
},
"bottom_right": {
"lat": 40.70,
"lon": -73.98
}
}
}
}
}

Geo Distance Query

The geo_distance query finds documents with geo-points within a certain distance of a central point. To search for coffee shops within 1km of latitude 40.715 and longitude -74.011:

GET /coffee-shops/_search
{
"query": {
"geo_distance": {
"distance": "1km",
"location": {
"lat": 40.715,
"lon": -74.011
}
}
}
}

Geo Polygon Query

The geo_polygon query finds documents with geo-points within an arbitrary polygon. It‘s useful for querying more complex shapes than circles or boxes.

GET /coffee-shops/_search
{
"query": {
"geo_polygon": {
"location": {
"points": [
{"lat" : 40.720740, "lon" : -74.025002},
{"lat" : 40.707187, "lon" : -74.031162},
{"lat" : 40.711958, "lon" : -73.981224},
{"lat" : 40.726552, "lon" : -73.981421}
] }
}
}
}

Sorting by Distance

When presenting geolocation search results, it‘s often useful to sort the matches by their distance from the search origin. We can achieve this in Elasticsearch by using script fields.

Suppose we want to sort coffee shops by their distance from the user‘s location, given as a [latitude, longitude] array in the "origin" parameter:

GET /coffee-shops/_search
{
"query": {
"match_all": {}
},
"script_fields": {
"distance": {
"script": {
"source": "doc[‘location‘].arcDistance(params.origin)",
"params": {
"origin": [40.715, -74.011] }
}
}
},
"sort": [
{
"_geo_distance": {
"location": [40.715, -74.011],
"order": "asc",
"unit": "m"
}
}
] }

Here we first use script_fields to calculate the distance between each coffee shop and the origin point. Then we sort the results by this calculated distance in ascending order.

Aggregating Geospatial Data

Elasticsearch also allows us to run aggregations on geospatial data. Geo aggregations group documents into buckets based on their geographical locations.

A common use case is to display search results on a map with gridded boxes, where each grid cell shows the number of results within that cell. This can be done using the geohash_grid aggregation.

GET /coffee-shops/_search
{
"size": 0,
"aggregations": {
"grid": {
"geohash_grid": {
"field": "location",
"precision": 5
}
}
}
}

The above query divides the geographical area into a grid of cells 5 geohash lengths in size. The "size": 0 parameter tells Elasticsearch to only return the aggregation results, without any hits.

The response will contain a list of grid cells with the number of coffee shops in each:

{

"aggregations": {
"grid": {
"buckets": [
{
"key": "dr5rs",
"doc_count": 1
},
{
"key": "dr5ru",
"doc_count": 2
}
] }
}
}

Integrating Elasticsearch in a Java Application

To use Elasticsearch in a Java app, you‘ll need to add the Java High Level REST Client to your project dependencies. For a Maven project, include this in your pom.xml:

org.elasticsearch.client
elasticsearch-rest-high-level-client
7.12.0

Then you can instantiate a RestHighLevelClient instance and use it to interact with your Elasticsearch cluster:

RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("localhost", 9200, "http")
)
);

// Index a document
IndexRequest request = new IndexRequest("coffee-shops");
request.source("{ \"name\":\"Coffee Shop D\", \"location\":\"40.2, -73.1\" }",
XContentType.JSON);
IndexResponse response = client.index(request, RequestOptions.DEFAULT);

// Search for documents
SearchRequest request = new SearchRequest("coffee-shops");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.geoDistanceQuery("location")
.point(40.715, -74.011)
.distance(1, DistanceUnit.KILOMETERS));
request.source(sourceBuilder);

SearchResponse response = client.search(request, RequestOptions.DEFAULT);

// Close the client when done
client.close();

For a more complete example, let‘s build a simple store locator using Spring Boot and Elasticsearch. The application will have a single endpoint that accepts a user‘s latitude and longitude and returns a list of the closest coffee shops.

@SpringBootApplication
@RestController
public class LocationSearchApplication {

private RestHighLevelClient client;

@Value("${elasticsearch.host}")
private String esHost;

@Value("${elasticsearch.port}")
private int esPort;

@PostConstruct
public void init() {
client = new RestHighLevelClient(
RestClient.builder(
new HttpHost(esHost, esPort, "http")));
}

@GetMapping("/nearby-shops")
public List<Map<String, Object>> nearbyShops(
@RequestParam double lat,
@RequestParam double lon) {

SearchRequest request = new SearchRequest("coffee-shops");  

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.geoDistanceQuery("location")
  .point(lat, lon) 
  .distance(1, DistanceUnit.KILOMETERS));
sourceBuilder.size(5);
sourceBuilder.fetchSource(new String[]{"name", "location"}, null);

request.source(sourceBuilder);

try {
  SearchResponse response = client.search(request, RequestOptions.DEFAULT);

  return Arrays.stream(response.getHits().getHits())
    .map(hit -> {
      Map<String, Object> result = new HashMap<>(); 
      result.put("name", hit.getSourceAsMap().get("name"));
      result.put("location", hit.getSourceAsMap().get("location")); 
      return result;
    })
    .collect(Collectors.toList()); 
} catch (IOException e) {
  throw new RuntimeException(e);
} 

}

@PreDestroy
public void cleanup() throws IOException {
client.close();
}

public static void main(String[] args) {
SpringApplication.run(LocationSearchApplication.class, args);
}
}

The /nearby-shops endpoint expects "lat" and "lon" parameters representing the user‘s location. It constructs a geo_distance query to find coffee shops within 1km of the user, limiting the results to 5. The Elasticsearch response is then transformed into a list of maps containing just the "name" and "location" fields before being returned to the caller.

To see the app in action, start it with mvn spring-boot:run and visit http://localhost:8080/nearby-shops?lat=40.715&lon=-74.011. You should get back a JSON array of the 5 closest coffee shops to that location.

Tips and Best Practices

Here are a few tips to keep in mind when working with geolocation search in Elasticsearch:

  • Use a geohash level of precision appropriate for your use case. A shorter geohash gives larger, less precise grid cells, while a longer one gives smaller, more precise cells. For most applications, a precision between 4 and 7 is usually sufficient.

  • Combine geo filters with non-geo filters for more targeted searches. For example, you might filter coffee shops by location and a certain tag or rating.

  • Use geo aggregations to analyze the geographical distribution of your data. You can use metrics aggregations to calculate stats like the average rating of coffee shops in each grid cell.

  • Optimize your Elasticsearch queries for better search performance. Use filters instead of queries where possible, and limit the size of your responses to only what the application needs.

  • Regularly monitor and tune your Elasticsearch cluster to ensure optimal indexing and search speed. Use tools like Kibana or Elastic APM for visibility into performance metrics.

Conclusion

In this guide, we covered the fundamentals of setting up geolocation search with Elasticsearch. We walked through the process of installing Elasticsearch, creating an index with geo mapping, indexing and querying geospatial data, and building a sample store locator app with Spring Boot.

But this is just the tip of the iceberg – Elasticsearch has many more geospatial capabilities we didn‘t cover. Some areas to explore further include:

  • The geo_shape data type for indexing and querying complex shapes like polygons and linestrings
  • Additional geo aggregations like geo-centroid and geo-bounds
  • Integration with mapping and visualization tools like Kibana Lens or Elastic Maps

The best way to deepen your understanding is to experiment with your own data and use cases. Try indexing different types of geospatial data, run geo queries and aggregations to analyze patterns and trends, and prototype a location-enabled feature for your own applications.

As you dive deeper into geolocation search with Elasticsearch, you‘ll find that the possibilities are virtually endless. And equipped with the knowledge from this guide, you‘re now ready to begin unlocking the power of location-based search for your own projects. Happy geo-hacking!

Similar Posts