Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

RavenDb Spatial Query

Tags:

c#

.net

ravendb

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.

like image 417
Louis Lewis Avatar asked Apr 18 '21 13:04

Louis Lewis


People also ask

How do you query in Ravendb studio?

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 .

What is a spatial query?

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.


1 Answers

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:

Spatial query results, map view

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:

Map representation of previous WKT

You can then use the same style of query as before, but be aware:

  • Your 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.
  • You need to be aware of the order of points in WKT. It matters. You need counter clockwise.
like image 174
Ayende Rahien Avatar answered Oct 09 '22 10:10

Ayende Rahien