We are moving from MongoDb to RavenDb, and ran into an issue.
We have a simple model (C# DotNet)
using System.Globalization;
public class Location
{
#region Properties
public double Latitude { get; set; }
public double Longitude { get; set; }
#endregion Properties
#region Methods
public override string ToString()
{
var numberFormatInfo = new NumberFormatInfo { NumberDecimalSeparator = "." };
return $"{Longitude.ToString(numberFormatInfo)} {Latitude.ToString(numberFormatInfo)}";
}
#endregion Methods
}
public class Place
{
public Location[] Area {get;set;} // Please note the array
}
A customer can either select a place as a single pair, or they can draw a polygon on an interactive map, which represents the place, that would get stored in the Area property above.
In MongoDb this could not have been simpler
var locationQuery = new FilterDefinitionBuilder<Place>()
.GeoWithinBox(
field: x => x.Area,
lowerLeftX: query.Criteria.LongitudeBottomLeft,
lowerLeftY: query.Criteria.LatitudeBottomLeft,
upperRightX: query.Criteria.LongitudeTopRight,
upperRightY: query.Criteria.LatitudeTopRight);
The query class above, represents the currently visible area on the map, the customer is looking at.
Despite my best attempts, I cannot decipher the documentation on how to achieve the same. I am hoping someone may be able to shed some light on the matter, on how one would achieve this using the RavenDb Dotnet client (RavenDB.Client)
Thanks
Update & Sample!
After the great answer below from Ayende Rahien, I thought I would add some details here in attempt to simplify the journey for others landing here.
This is what the index looked like.
using System.Linq;
using Raven.Client.Documents.Indexes;
public class PlaceAreaIndex : AbstractIndexCreationTask<Place>
{
public PlaceAreaIndex()
{
Map = places => from place in places
select new
{
Area = place.Area.Select(location => CreateSpatialField(location.Latitude, location.Longitude))
};
}
}
You will need to register your index on the server, before using the query.
new PlaceAreaIndex().Execute(store);
I added some helper classes to assist me testing this out, I am adding them here so the sample gets you as close as possible to a running code
public class Box
{
#region Constructor
public Box(Location topRight, Location bottomLeft)
{
TopRight = topRight;
BottomLeft = bottomLeft;
TopLeft = new Location { Latitude = topRight.Latitude, Longitude = bottomLeft.Longitude };
BottomRight = new Location { Latitude = bottomLeft.Latitude, Longitude = topRight.Longitude };
}
#endregion Constructor
#region Properties
public Location TopRight { get;}
public Location TopLeft { get; }
public Location BottomLeft { get; }
public Location BottomRight { get; }
#endregion Properties
#region Methods
public override string ToString()
{
return $"POLYGON (({TopRight}, {TopLeft}, {BottomLeft}, {BottomRight}, {TopRight}))";
}
#endregion Methods
}
Then using the helpers
var topRight = Location.New(latitude: -22.674847351188916, longitude: 31.25061035156253);
var bottomLeft = Location.New(latitude: -28.9600886880069, longitude: 25.1422119140623);
var box = new Box(topRight: topRight, bottomLeft: bottomLeft);
After that you can query it like this
var places = await session
.Query<Place, PlaceAreaIndex>()
.Spatial(
factory => factory.Area,
criteria => criteria.RelatesToShape(
shapeWkt: box.ToString(),
relation: SpatialRelation.Within))
.ToArrayAsync();
Hope it helps you.
In the Studio, create a new database. Go to Settings , then to Create Sample Data , and click the big Create button. This will create a sample database (the Northwind online shop data) that we can query. Now, go to Indexes and then List of Indexes .
Spatial query refers to the process of retrieving a data subset from a map layer by working directly with the map features. In a spatial database, data are stored in attribute tables and feature/spatial tables.
IIUC, the issue is that you may have a specific location or a polygon, right? When representing a polygon in RavenDB, you need to use WKT for that.
For example, here is what this looks like:
POLYGON((14.316406249999982 37.541615344157876,32.68554687499998 37.541615344157876,32.68554687499998 25.439901234431595,14.316406249999982 25.439901234431595,14.316406249999982 37.541615344157876))
This can then be used by the spatial engine inside of RavenDB and allow you to query using spatial.Contains
.
However, that might not be what you want, depending on usage.
If what you want is simply to find all the Places
that has a Location inside a rectangle, it would be easier to just threat this as an array of locations.
On the other hand, if this is really a polygon, that is different.
Might be easier to show things. The sample data in RavenDB has spatial data, like so:
$wkt = "POLYGON((-1.0800308814195025 51.942980464942416,0.5789046654554975 51.942980464942416,0.5789046654554975 51.01935531918276,-1.0800308814195025 51.01935531918276,-1.0800308814195025 51.942980464942416))"
from Employees
where spatial.within( spatial.point(Address.Location.Latitude , Address .Location.Longitude),
spatial.wkt($wkt))
And you can then see the new spatial tab which will show you the results:
This shows the rectangle (wkt) and a single point.
However, if you have an array, you can also use:
"Locations": [
{
"Latitude": 51.52384070000001,
"Longitude": -0.0944233
},
{
"Latitude": 51.48145355123356,
"Longitude": -0.17475717569860105
}
],
That, however, need an explicit index to handle, you can write it like so:
from e in docs.Employees
select new
{
Locations = e.Locations.Select(l => CreateSpatialField(l.Latitude, l.Longitude))
}
This index all individual locations for an employee.
And then you can query it using:
$wkt = "POLYGON((-1.0800308814195025 51.942980464942416,0.5789046654554975 51.942980464942416,0.5789046654554975 51.01935531918276,-1.0800308814195025 51.01935531918276,-1.0800308814195025 51.942980464942416))"
from index 'Employees/ByLocations'
where spatial.within( Locations, spatial.wkt($wkt))
The other option, however, is when you need to turn the list of locations into a ploygon.
For example, consider this WKT:
POLYGON((-0.09345874990495329 51.7543103056377,-0.6823256691840518 51.56348306507395,-0.2252946874049533 51.30046869394702,0.09990102742639895 51.42120466591834,-0.09345874990495329 51.7543103056377))
This represents this polygon:
You can then use the same style of query as before, but be aware:
contains
query now need to include the entire polygon. You can use intersect
as well, but then you are probably better off with the single points option.If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With