Part 5 - Geospatial queries

Following the previous part in our Spring Data demo, where we had a look at repository methods with self written AQL, we will now take a look at Geospatial queries.

Geospatial queries are a subsection of derived queries. To use a Geospatial query on a collection, a geo index must exist on that collection. A geo index can be created on a field which is a two element array, corresponding to latitude and longitude coordinates.

As a subsection of derived queries, Geospatial queries support the same return types, and also these additional three return types: GeoPageGeoResult and Georesults. These types must be used in order to get the distance of each document as generated by the query.

Geo data modeling

To demonstrate Geospatial queries we create a new entity class Location with a field location of type org.springframework.data.geo.Point. We also have to create a geo index on this field. We can do so by annotating the field with @GeoIndexed(geoJson = true). As you probably remember we have already used an index in our Character class but we annotated the type and not the affected fields. Spring Data ArangoDB offers two ways of defining an index. With @<IndexType>Indexed annotations indexes for single fields can be defined. If the index should include multiple fields the @<IndexType>Index annotations can be used on the type instead. Take a look here for more information.

Create a new class Location:

package com.arangodb.spring.demo.entity;
 
import com.arangodb.springframework.annotation.Document;
import com.arangodb.springframework.annotation.GeoIndexed;
import org.springframework.data.annotation.Id;
import org.springframework.data.geo.Point;
 
import java.util.Arrays;
 
@Document("locations")
public class Location {
 
    @Id
    private String id;
    private final String name;
    @GeoIndexed(geoJson = true)
    private final Point location;
 
    public Location(final String name, final Point location) {
        super();
        this.name = name;
        this.location = location;
    }
 
    // getter & setter
 
}

and the corresponding repository LocationRepository:

package com.arangodb.spring.demo.repository;
 
import com.arangodb.spring.demo.entity.Location;
import com.arangodb.springframework.repository.ArangoRepository;
 
public interface LocationRepository extends ArangoRepository<Location, String> {
 
}

After that we create a new CommandLineRunner, add it to our DemoApplication and perform some insert operations with some popular locations from Game of Thrones with the coordinates of their real counterparts.

package com.arangodb.spring.demo.runner;
 
import com.arangodb.spring.demo.entity.Location;
import com.arangodb.spring.demo.repository.LocationRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
 
import java.util.Arrays;
 
public class GeospatialRunner implements CommandLineRunner {
 
    @Autowired
    private LocationRepository repository;
 
    @Override
    public void run(final String... args) throws Exception {
        System.out.println("# Geospatial");
 
        repository.saveAll(Arrays.asList(
                new Location("Dragonstone",     new Point(-6.815096, 55.167801)),
                new Location("King's Landing",  new Point(18.110189, 42.639752)),
                new Location("The Red Keep",    new Point(14.446442, 35.896447)),
                new Location("Yunkai",          new Point(-7.129532, 31.046642)),
                new Location("Astapor",         new Point(-9.774249, 31.50974)),
                new Location("Winterfell",      new Point(-5.581312, 54.368321)),
                new Location("Vaes Dothrak",    new Point(-6.096125, 54.16776)),
                new Location("Beyond the wall", new Point(-21.094093, 64.265473))));
    }
}

DemoApplication:

Class<?>[]runner=new Class<?>[]{
        CrudRunner.class,
        ByExampleRunner.class,
        DerivedQueryRunner.class,
        RelationsRunner.class,
        AQLRunner.class,
        GeospatialRunner.class
};

There are two kinds of Geospatial query, Near and Within.

Near

Near sorts entities by distance from a given point. The result can be restricted with paging.

LocationRepository:

System.out.println("## Find the first 5 locations near 'Winterfell'");
GeoPage first5 = repository.findByLocationNear(new Point(-5.581312, 54.368321), PageRequest.of(0, 5));
first5.forEach(System.out::println);
 
System.out.println("## Find the next 5 locations near 'Winterfell' (only 3 locations left)");
GeoPage next5 = repository.findByLocationNear(new Point(-5.581312, 54.368321), PageRequest.of(1, 5));
next5.forEach(System.out::println);

Because we used the coordinates of Winterfell the distance in the output of Winterfell is 0.

## Find the first 5 locations near 'Winterfell'
GeoResult [content: Location [id=14404, name=Yunkai, location=[31.046642, -7.129532]], distance: 3533.2076972451478 KILOMETERS, ]
GeoResult [content: Location [id=14405, name=Astapor, location=[31.50974, -9.774249]], distance: 3651.785495816579 KILOMETERS, ]
GeoResult [content: Location [id=14403, name=The Red Keep, location=[35.896447, 14.446442]], distance: 4261.971994059222 KILOMETERS, ]
GeoResult [content: Location [id=14402, name=King's Landing, location=[42.639752, 18.110189]], distance: 5074.755682897005 KILOMETERS, ]
GeoResult [content: Location [id=14407, name=Vaes Dothrak, location=[54.16776, -6.096125]], distance: 6049.156388427102 KILOMETERS, ]
## Find the next 5 locations near 'Winterfell' (only 3 locations left)
GeoResult [content: Location [id=14406, name=Winterfell, location=[54.368321, -5.581312]], distance: 6067.104268175527 KILOMETERS, ]
GeoResult [content: Location [id=14401, name=Dragonstone, location=[55.167801, -6.815096]], distance: 6165.650581599857 KILOMETERS, ]
GeoResult [content: Location [id=14408, name=Beyond the wall, location=[64.265473, -21.094093]], distance: 7350.229798961836 KILOMETERS, ]

Within

Within both sorts and filters entities, returning those within the given distance, range or shape.

Lets add some methods to LocationRepository which use different filter criteria.

GeoResults findByLocationWithin(Point location, Distance distance);
 
Iterable findByLocationWithin(Point location, Range distanceRange);

With these methods we can search for locations within a given distance or range to our point – Winterfell.

System.out.println("## Find all locations within 50 kilometers of 'Winterfell'");
GeoResults findWithing50kilometers = repository
    .findByLocationWithin(new Point(-5.581312, 54.368321), new Distance(50, Metrics.KILOMETERS));
findWithing50kilometers.forEach(System.out::println);
 
System.out.println("## Find all locations which are 40 to 50 kilometers away from 'Winterfell'");
Iterable findByLocationWithin = repository.findByLocationWithin(new Point(-5.581312, 54.368321),
    Range.of(Range.Bound.inclusive(40000.), Range.Bound.exclusive(50000.)));
findByLocationWithin.forEach(System.out::println);

As you can see in our console output, both ‘Winterfell’ and ‘Vaes Dothrak’ are located within a 50 kilometers radius around our point. But only ‘Vaes Dothrak’ is obviously more than 40 kilometers away from it.

## Find all locations within 50 kilometers of 'Winterfell'
GeoResult [content: Location [id=14843, name=Winterfell, location=[54.368321, -5.581312]], distance: 0.0 KILOMETERS, ]
GeoResult [content: Location [id=14844, name=Vaes Dothrak, location=[54.16776, -6.096125]], distance: 40.186277071065994 KILOMETERS, ]
## Find all locations which are 40 to 50 kilometers away from 'Winterfell'
Location [id=14844, name=Vaes Dothrak, location=[54.16776, -6.096125]]

But we can not only implement geo functions going from a single point, but it is also possible to search for locations within a polygon.

In our last example we add a method using Polygon.

LocationRepository:

Iterable findByLocationWithin(Polygon polygon);

GeospatialRunner:

System.out.println("## Find all locations within a given polygon");
Iterable withinPolygon = repository.findByLocationWithin(
        new Polygon(Arrays.asList(new Point(-25, 40), new Point(-25, 70), new Point(25, 70), new Point(-25, 40))));
withinPolygon.forEach(System.out::println);

The console output should be:

## Find all locations within a given polygon
Location [id=16922, name=Beyond the wall, location=[64.265473, -21.094093]]

That’s it! You should now have an overview of the possibilities with Spring Data ArangoDB.