Spatial Search

To support the ability to retrieve the data based on spatial coordinates, the spatial search has been introduced.

Creating Indexes

To take an advantage of the spatial search, first we need to create an index with a spatial field. To mark field as the spatial field, we need to use SpatialGenerate method:

object SpatialGenerate(double lat, double lng);

object SpatialGenerate(string fieldName, double lat, double lng);

object SpatialGenerate(string fieldName, string shapeWKT);

object SpatialGenerate(string fieldName, string shapeWKT, SpatialSearchStrategy strategy);

object SpatialGenerate(string fieldName, string shapeWKT, SpatialSearchStrategy strategy, int maxTreeLevel);

public enum SpatialSearchStrategy
{
	GeohashPrefixTree,
	QuadPrefixTree,
}

where:

  • fieldName is a name of the field containing the shape to use for filtering (if the overload with no fieldName is used, then the name is set to default value: __spatial)
  • lat/lng are latitude/longitude coordinates
  • shapeWKT is a shape in WKT format
  • strategy is a spatial search strategy (default: GeohashPrefixTree)
  • maxTreeLevel is a integer that indicates the maximum number of levels to be used in PrefixTree and controls the precision of shape representation (9 for GeohashPrefixTree and 23 for QuadPrefixTree)

In our example we will use Event class and very simple index defined below.

public class Event
{
	public string Id { get; set; }

	public string Name { get; set; }

	public double Latitude { get; set; }

	public double Longitude { get; set; }
}

public class Events_SpatialIndex : AbstractIndexCreationTask<Event>
{
	public Events_SpatialIndex()
	{
		Map = events => from e in events
						select new
						{
							Name = e.Name,
							__ = SpatialGenerate("Coordinates", e.Latitude, e.Longitude)
						};
	}
}

If our Event would contain the WKT property already:

public class EventWithWKT
{
	public string Id { get; set; }

	public string Name { get; set; }

	public string WKT { get; set; }
}

then we could define our field using Spatial method in AbstractIndexCreationTask:

public class EventsWithWKT_SpatialIndex : AbstractIndexCreationTask<EventWithWKT>
{
	public EventsWithWKT_SpatialIndex()
	{
		Map = events => from e in events
						select new
						{
							Name = e.Name,
							WKT = e.WKT
						};

		Spatial(x => x.WKT, options => options.Geography.Default());
	}
}

where under options we got access to our geography and cartesian factories:

public class SpatialOptionsFactory
{
	public GeographySpatialOptionsFactory Geography;

	public CartesianSpatialOptionsFactory Cartesian;
}

GeographySpatialOptionsFactory:

// GeohashPrefixTree strategy with maxTreeLevel set to 9
SpatialOptions Default(SpatialUnits circleRadiusUnits = SpatialUnits.Kilometers);

SpatialOptions BoundingBoxIndex(SpatialUnits circleRadiusUnits = SpatialUnits.Kilometers);

SpatialOptions GeohashPrefixTreeIndex(int maxTreeLevel, SpatialUnits circleRadiusUnits = SpatialUnits.Kilometers);

SpatialOptions QuadPrefixTreeIndex(int maxTreeLevel, SpatialUnits circleRadiusUnits = SpatialUnits.Kilometers);

CartesianSpatialOptionsFactory:

SpatialOptions BoundingBoxIndex();

SpatialOptions QuadPrefixTreeIndex(int maxTreeLevel, SpatialBounds bounds);

Spatial search strategies

I. GeohashPrefixTree

Geohash is a latitude/longitude representation system that describes earth as a grid with 32 cells, assigning to each grid cell an alphanumeric character. Each grid cell is divided further into 32 smaller chunks and each chunk has also an alphanumeric character assigned and so on.

E.g. The location of a 'New York' in United States is represented by following geohash: DR5REGY6R and it represents the 40.7144 -74.0060 coordinates. Removing characters from the end of geohash will decrease the precision level.

More information about geohash uses, decoding algorithm and limitations can be found here.

II. QuadPrefixTree

QuadTree represents earth as a grid with exactly four cells and similarly to geohash, each grid cell (sometimes called bucket) has a letter assigned and is divided further into 4 more cells and so on.

More information about QuadTree can be found here.

Note

GeohashPrefixTree is a default SpatialSearchStrategy. Doing any changes to the strategy after index has been created will trigger re-indexation process.

III. BoundingBox

Precision

By default the precision level (maxTreeLevel) for GeohashPrefixTree is set to 9 and for QuadPrefixTree the value is 23, which means that the coordinates are represented by a 9 or 23 character string. The difference exists, because the QuadTree representation would be much less precise if the level would be the same.

A. Geohash precision values (from unterbahn.com).

Level E-W distance at equator N-S distance at equator
12 ~3.7cm ~1.8cm
11 ~14.9cm ~14.9cm
10 ~1.19m ~0.60m
9 ~4.78m ~4.78m
8 ~38.2m ~19.1m
7 ~152.8m ~152.8m
6 ~1.2km ~0.61km
5 ~4.9km ~4.9km
4 ~39km ~19.6km
3 ~157km ~157km
2 ~1252km ~626km
1 ~5018km ~5018km

B. Quadtree precision values

Level Distance at equator
30 ~4cm
29 ~7cm
28 ~15cm
27 ~30cm
26 ~60cm
25 ~1.19m
24 ~2.39m
23 ~4.78m
22 ~9.56m
21 ~19.11m
20 ~38.23m
19 ~76.23m
18 ~152.92m
17 ~305.84m
16 ~611.67m
15 ~1.22km
14 ~2.45km
13 ~4.89km
12 ~9.79km
11 ~19.57km
10 ~39.15km
9 ~78.29km
8 ~156.58km
7 ~313.12km
6 ~625.85km
5 ~1249km
4 ~2473km
3 ~4755km
2 ~7996km
1 ~15992km

Beside the Event class let us add SpatialDoc with a corresponding index to show how to do a strongly-typed spatial query using Spatial method.

public class SpatialDoc_Index : AbstractIndexCreationTask<SpatialDoc>
{
	public SpatialDoc_Index()
	{
		Map = docs => from spatial in docs
					  select new
					  {
						  Shape = spatial.Shape,
						  Point = spatial.Point
					  };

		Spatial(x => x.Shape, options => options.Geography.Default());
		Spatial(x => x.Point, options => options.Cartesian.BoundingBoxIndex());
	}
}

session.Query<SpatialDoc, SpatialDoc_Index>()
	.Spatial(x => x.Shape, criteria => criteria.WithinRadiusOf(500, 30, 30))
	.ToList();

session.Query<SpatialDoc, SpatialDoc_Index>()
	.Spatial(x => x.Shape, criteria => criteria.Intersects(someWktShape))
	.ToList();

The methods available under criteria are:

SpatialCriteria RelatesToShape(object shape, SpatialRelation relation);

SpatialCriteria Intersects(object shape);

SpatialCriteria Contains(object shape);

SpatialCriteria Disjoint(object shape);

SpatialCriteria Within(object shape);

SpatialCriteria WithinRadiusOf(double radius, double x, double y);

The most basic usage and probably most common one is to search for all points or shapes within provided distance from the given center point. To perform this search we will use WithinRadiusOf method that is a part of query customizations.

session.Query<Event, Events_SpatialIndex>()
	   .Customize(x => x.WithinRadiusOf(
		   fieldName: "Coordinates",
		   radius: 10,
		   latitude: 32.1234,
		   longitude: 23.4321))
	   .ToList();

The method can be used also when using LuceneQuery.

session.Advanced.LuceneQuery<Event, Events_SpatialIndex>()
	   .WithinRadiusOf(fieldName: "Coordinates", radius: 10, latitude: 32.1234, longitude: 23.4321)
	   .ToList();

The WithinRadiusOf method is a wrapper around RelatesToShape method.

IDocumentQueryCustomization RelatesToShape(string fieldName, string shapeWKT, SpatialRelation rel);

public enum SpatialRelation
{
	Within,
	Contains,
	Disjoint,
	Intersects,

	/// <summary>
	/// Does not filter the query, merely sort by the distance
	/// </summary>
	Nearby
}

where first parameter is a name of the field containing the shape to use for filtering, next one is a shape in WKT format and the last one is a spatial relation type.

So to perform a radius search from the above example and use RelatesToShape method, we do as follows

session.Query<Event, Events_SpatialIndex>()
		.Customize(x => x.RelatesToShape("Coordinates", "Circle(32.1234, 23.4321, d=10.0000)", SpatialRelation.Within))
		.ToList();

or when we want to use LuceneQuery then

session.Advanced.LuceneQuery<Event, Events_SpatialIndex>()
	   .RelatesToShape("Coordinates", "Circle(32.1234, 23.4321, d=10.0000)", SpatialRelation.Within)
	   .ToList();

Warning

From RavenDB 2.0 the distance by default is measured in kilometers in contrast to the miles used in previous versions.

Format support

From version 2.5 RavenDB also supports indexing of GeoJSON objects.

var point = new
				{
					type = "Point",
					coordinates = new[] { -10d, 45d }
				};

session.Store(new SpatialDoc { Shape = point });

Beside the WKT and GeoJSON following formats are also supported:

session.Store(new SpatialDoc { Point = new[] { -10d, 45d } });
session.Store(new SpatialDoc { Point = new { X = -10d, Y = 45d } });
session.Store(new SpatialDoc { Point = new { Latitude = 45d, Longitude = -10d } });
session.Store(new SpatialDoc { Point = new { lat = 45d, lon = -10d } });
session.Store(new SpatialDoc { Point = new { lat = 45d, lng = -10d } });
session.Store(new SpatialDoc { Point = new { Lat = 45d, Long = -10d } });
session.Store(new SpatialDoc { Point = "geo:45.0,-10.0;u=2.0" }); // Geo URI

Third-party spatial library integration

To integrate with other spatial libraries, the document store must be configured to use a custom library-specific JsonConverter which reads/writes WKT or GeoJSON.

Examples of such converters can be found at Simon Bartlett's github repository page.