Implementing Geo features in GraphQL

Motivation

Supporting Geolocation features natively in GraphQL so that users of the GraphQL API can benefit from them.

User Impact

Users can directly use these features without having to define their own custom types and using custom queries.

Implementation

https://dgraph.io/docs/query-language/#geolocation shows that Dgraph supports storing Point, Polygon, MultiPolygon geo types. We need to be able to support near, within, contains and intersects query filter. We can have custom scalars for these within our predefined GraphQL schema like below.

Custom scalars

Inside GraphQL±, a predicate which has geo type can store data for a Point, Polygon or MultiPolygon but since GraphQL is strongly typed, a field inside a type will be able to only store one type of data.

type Point {
  Latitude: Float!
  Longitude: Float!
}

type PointList {
  Points: [Point!]!
}

type Polygon {
  Coordinates: [PointList!]!
}

type MultiPolygon {
  Coordinates: [Polygon!]!
}

An example of using custom scalars

Say, we have a hotel type with has a location and area, something like below.

type Hotel {
  id: ID!
  name: String!
  location: Point
  // TODO - See if MultiPolygon has different behaviour from Polygon.
  area: Polygon
}

In the Dgraph schema that is generated, we would automatically add an index to these fields like below.

Hotel.location: geo @index(geo) .
Hotel.area: geo @index(geo) .

Mutations

For this AddHotelInput and HotelPatch would contain location and area.

input AddHotelInput {
  name: String!
  location: Point
  area: Polygon
}

input AuthorPatch {
  name: String!
  location: Point
  area: Polygon
}

Queries

We would generate the Geo queries as part of the Hotel filter so that it can be combined with other filters.

input NearFilter {
    Distance: Float!
    Coordinate: Point!
}

input WithinFilter {
    # TODO - Verify if we also allow searching with a MultiPolygon, the docs are not very clear about this.
    polygon: Polygon!
}

input ContainsFilter {
    # The user should be giving one of these.
    # TODO - Verify if this can also accept MultiPolygon as an input.
    point: Point
    polygon: Polygon
}

input IntersectsFilter {
    # The user should be giving one of these.
    polygon: Polygon
    mulitPolygon: MultiPolygon
}

input PointGeoFilter {
    near: NearFilter
    within: WithinFilter
}

input PolygonGeoFilter {
    near: NearFilter
    within: WithinFilter
    contains: ContainsFilter
    intersects: IntersectsFilter
}

input HotelFilter {
    location: PointGeoFilter
    area: PolygonGeoFilter
    and: HotelFilter
    or: HotelFilter
    not: HotelFilter
}

near

GraphQL± Definition - Matches all entities where the location given by predicate is within distance meters of geojson coordinate [long, lat] .

graphql+-

{
  tourist(func: near(loc, [-122.469829, 37.771935], 1000) ) {
    name
  }
}

graphql

queryHotel(filter: {
    location: { 
        near: {
            coordinate: {
                latitute: 37.771935, 
                longitude: -122.469829
            }, 
            distance: 1000
        }
    }
}) {
  name
}

Note, the near query can be used for returning Point as well as Polygon, MultiPolygon that are near a point. So if the user used area instead of location, it should return Polygon’s near the given point.

within

GraphQL± Definition - Matches all entities where the location given by predicate lies within the polygon specified by the geojson coordinate array.

graphql+-

{
  tourist(func: within(loc, [[[....]]] )) {
    name
  }
}

graphql

queryHotel(filter: {
    location: { 
        within: {
            polygon: {
                coordinates: [[[....]]],
            }
        }
    }
}) {
  name
}

The within query would allow searching for all geo entities (point, polygon) within a polygon. So they would be generated for all types.
// TODO(pawan) - Verify if we support within queries for searching in a multipolygon.

contains

GraphQL± Definition - Matches all entities where the polygon describing the location given by predicate contains geojson coordinate [long, lat] or given geojson polygon.

ContainsFilter would only be generated for Polygon/MultiPolygon.

graphql+-

{
  tourist(func: contains(loc, [ -122.50326097011566, 37.73353615592843 ] )) {
    name
  }
}

graphql

queryHotel(filter: {
    area: { 
        contains: {
            point: {
                coordinates: [],
            }
        }
    }
}) {
  name
}

intersects

GraphQL± Definition - Matches all entities where the polygon describing the location given by predicate intersects the given geojson polygon.

IntersectsFilter would only be generated for Polygon/MultiPolygon.

graphql+-

{
  tourist(func: intersects(loc, [[[...]]] )) {
    name
  }
}

graphql

queryHotel(filter: {
    area: { 
        intersects: {
            polygon: {
                coordinates: [[[...]]],
            }
        }
    }
}) {
  name
}

Needs further research


  • Understanding of which functions are supported for MultiPolygon type. The docs don’t shed much light on this and we would have to dig deeper into the code to find this out.

References


2 Likes

I think we’d also want order by

1 Like

We already have order by in GraphQL. Were you thinking of ordering by distance in the case of a near query?

yes, ordering by distance from some point

1 Like

We don’t have that in GraphQL± yet if I recall correctly, so we’ll skip that for the first pass and just focus on implementing the equivalent of what we already have. We can certainly implement the ordering after that.

1 Like

I think a user would end up using variables for these things. So my query in code would look like

query findHotel($at: Point, $distance: Float) {
queryHotel(filter: {
    location: { 
        near: {
            coordinate: $at, 
            distance: $distance
        }
    }
}) {
  name
}
}

Which seems pretty nice.

Definitely think we should only support what’s in Dgraph already. If any extra requirements come in. It’d probably have to be supported in Dgraph first, then bubbled into GraphQL.

One other thing I’d check is what sort of data structures do front end tooling that already does geo support - e.g. if there’s common JS libs in GraphQL for adding any of this to your app.

Also - Let’s move this to public so community can see

4 Likes

Guys, this is absolutely fantastic.

FYI, you have a few copy/paste issues (naming is incorrect in a few places) but what you’ve written up here as a spec is exactly what I believe would suit our purposes. Incredible stuff!

The only thing I don’t see is pagination, but I don’t see that in the GQL docs either so that might not be a thing?

4 Likes

Thanks for reading carefully enough to pick up the copy/paste errors! I think we’ll try to rewrite that top post collating any comments as we go, so we should :crossed_fingers: squash those.

As for pagination, we do support that in our GraphQL for the other queries, and it will ‘work’ here. However, I don’t think that sorting is supported in Dgraph’s geo queries at the moment (so you’d get pagination by node’s uid order). Some of them don’t have natural orderings anyway, e.g. within, but near probably could support and ordering.

We’ll look into it some more.

2 Likes

Love it! :heart: Awesome call, @michaelcompton .

Hi Team,

I kept on googling about this for a long time. Finally posted on @mrjn post asking about new enthusiasts in June, in hopes of getting an answer.

And lo and behold, there’s a discussion going on about it!

I love it and can’t wait for this to be out.

Oh what a time to be alive!

3 Likes

Any pagination is better than no pagination :slight_smile:

2 Likes

Hi all, has this been implemented?

2 Likes

Hey @machship-mm

This hasn’t been implemented yet but looks like there is a lot of interest on this RFC so we’ll prioritize working on this during Sept.

Great stuff! Can’t wait :slight_smile:

@arijit is starting work on this and it should land in master by the end of the month!

3 Likes

It is now in master branch, and will be part of 20.11 release.
PRs:

4 Likes