st_distance vs <-> in ash_geo for nearest neighbor search/filter (knn)
I'm just digging into
ash_geo
and attempting to implement a knn filter, as described here:
https://postgis.net/workshops/postgis-intro/knn.html
Unlike a distance search, the “nearest neighbour” search doesn’t include any measurement restricting how far away candidate geometries might be, features of any distance away will be accepted, as long as they are the nearest. PostgreSQL solves the nearest neighbor problem by introducing an “order by distance” (<->) operator that induces the database to use an index to speed up a sorted return set. With an “order by distance” operator in place, a nearest neighbor query can return the “N nearest features” just by adding an ordering and limiting the result set to N entries.I see the
st_distance
function, in both ash_geo
and geo_postgis
, but don't yet grok how to use it like the <->
operator, to get an unbounded, sorted result set.
I initially dove in with st_distance but then realized I was looking at something ~like:
The application idea is a user types in an address, I geocode that and get {long, lat}
, and run a knn against that to see the nearest points of interest.
Is there a nearest
function I'm overlooking or some other way to filter and order geometries? Manually run raw SQL?
Thanks!
(Sorry if this is the wrong place, I didn't see an ash_geo
tag)14 Replies
You can use
fragment
to embed a raw sql fragment in your expression, i.e fragment("? + ?", field1, field2)
, does that help?For kNN, you definitely want
order by ? <-> ?
, as the index performance is significantly better. The rest gets a little finnicky:
- Currently using this in a sort
clause requires that you create an expression-based calculation.
- This will need to be a module-based Ash.Calculation
, implementing expression/2
.
- Even module-based calcs only receive the context, but not a changeset, so you would need to use the set_context
change builtin.
- As far as I know, set_context
doesn't support the arg(:?)
syntax, but does accept an MFA which will receive the changeset, so you need to provide one that will extract the argument(s) you want.
- Your calc module can then use these to build the expression, which you can then sort on.
Take all of the above with one or two bits of salt, but either way it might be a more maintainable solution for now to do a manual read/modify_query
.FWIW, you can technically use
Ash.Calculation.new
and put that in a sort clause
calculations can accept arguments, also
load(score_plus_n: %{n: 10})
and sort: [score_plus_n: {:asc, %{n: 10}}]
This part seems fine:
But it looks like the arguments to the calculation in
load
are statically supplied in the example. The argument is meant to be a geometry (point) supplied as user input. Can load
, sort
or calculate
capture arguments from the action?Kind of.
But the idea is that you pass the values from the client into the call to load/sort for example
Like in gql those calculation arguments are added as arguments to the field
I have my system working!
The action:
The "modified" query (I guess I'm more making one from scratch using the ash argument):
I'm going to have to filter out some obviously bad data in addition to nils (i've got some null islands and some just... strange entries) but it's working!
(I'm not sure if this is optimal or the pros/cons vs a calculate-based approach)
I'd suggest using a calculation and a preparation, as opposed to modify_query.
I like this, because what I really want to say is:
Reading the body of the
prepare
fn
, this seems like it is closest to that.I'm getting
error: undefined variable "listing"
, reading the prepare
docs. Haven't used this part of Ash yet!
Is a benefit of using Ash primitives like preparations and calculations rather than modify_query that it will be simpler to integrate with other ash functionality like pagination?require Ash.Query
at the top
and yes, its exactly that kind of thing 🙂
oh, lol
Ash.Query.where
is not a thing
its Ash.Query.filter
, sorry 😆 have been writing ecto code recentlygiven that correction:
Nice, definitely wanna stay in-ecosystem for all the goodies 🙂
Sorry, remove
listing.
I copied it from your example and in ecto you use bindings, in Ash its implicitThe other benefit to this strategy is that you can also do things like:
You can return the distance
or
Ash.Query.filter(distance_from(location: location) < ^some_threshold)