Implementation in Elixir
Concept of implementing the filtering/sorting based on geo is going to be same, you can implement the same in any programming languages (e.g Javascript, Python). Here I would like to share, how we can implement the same in Elixir programming language.
Implement dynamic location filtering using PostGIS
In this section, we are exploring an example to see how provider dynamic location filtering will work in action with the PostGIS. And we are leveraging the library GeoPostGI for this implementation. I am assuming that you have mix project with PostgreSQL database configured already:
1. Enable/Create PostGIS Extension in PostgreSQL Schema
Be sure to enable the PostGIS extension if you haven't already done so (you might need DBA help to enable the extension if you are using AWS Aurora).
defmodule Data.Repo.Migrations.EnablePostgisExtension do
use Ecto.Migration
def change do
execute "CREATE EXTENSION IF NOT EXISTS postgis", "DROP EXTENSION IF EXISTS postgis"
end
end
2. Install GeoPostGIS library
The package can be installed by adding :geo_postgis
to your list of dependencies in mix.exs
.
def deps do
[
...,
{:geo_postgis, "~> 3.5"}
]
end
3. Add a column of type "geometry" in your table
Assuming you have enabled the PostGIS extension in your schema. Now you can create Ecto migration for creating a table with the geometry column.
defmodule Data.Repo.Migrations.CreateTableServiceProviders do
@moduledoc """
We are choosing `geography` data type although the calculations on a sphere are
computationally far more expensive than Cartesian calculations (geometry).
In our case, we are going to rely on `geography` type and in order to avoid
unnecessary heavy casting (while querying/indexing).
"""
use Ecto.Migration
def change do
create table("service_providers", primary_key: [name: id, type: :uuid] do
add :name, :string, null: false
add :verified_status, :string, null: false
...
add :physical_location_geopoints, :geography
end
end
end
4. Define a column with type geometry for Ecto Schema and extend the typing
defmodule Data.Schemas.ServiceProviderSchema do
@moduledoc """
Represents Ecto schema for 'service_providers' table.
"""
use Ecto.Schema
@primary_key {:global_id, :binary_id, autogenerate: false}
schema "service_providers" do
field :name, :string
field :verified_status, :string
...
# Spatial data type `Geo.PostGIS.Geometry` represents all the PostGIS types available
# for Geometry/Geography types ie, Geo.Point, Geo.MultiPoint.
# Cast all the physical locations long/lat in following struct:
# %Geo.MultiPoint{coordinates: [{0, 0}, {20, 20}], srid: 4326}
field :physical_location_geopoints, Geo.PostGIS.Geometry
end
end
Geo.PostGIS.Geometry
type is not available by default so we need to extend the Ecto typings by creating a new Module.
Postgrex.Types.define(
MyProject.PostgresTypes,
[Geo.PostGIS.Extension] ++ Ecto.Adapters.Postgres.extensions(),
json: Jason
)
and update the repo config:
config :my_project, Data.Repo,
database: "my_geo_search",
username: "my_geo_search",
hostname: "localhost",
types: MyProject.PostgresTypes
5. Insert multiple points (longitude/latitude) into the column
We can add a collection of points (longitude/latitude pair) into the column, we just need to cast a map to a struct in the following format:
%Geo.MultiPoint{coordinates: [{long, lat}], srid: 4326}
since our column type is Geo.PostGIS.Geometry and this can support all the Geometry types i.e. Point, MultiPoint, Polygon.
In PostGIS, Order of longitude and latitude matter in the pair, first should be longitude and then second should be latitude otherwise you might get incorrect results.
6. Write filters/sort query in PostGIS
Internally geo_postgis
function st_dwithin_in_meters
casts the geometries given to geographies to check for distance in meters if they are’t in geographies.
In order to filter & sort the Service provider by their closeness, we need center point ( means longitude/latitude) and radius in meters.
center = %Geo.Point{coordinates: {-97.733330, 30.266666}, srid: 4326}
radius = 50_000
query =
from sp in ServiceProviderSchema,
where: st_dwithin_in_meters(sp.physical_location_geopoints, ^center, ^radius),
order_by: [
st_distance_in_meters(sp.physical_location_geopoints, ^center),
desc: p.updated_at
],
select: %{id: sp.id}
7. Create Index to Speed-up the geo-distance queries in PostGIS
Creating a Spatial index to boost up the geospatial query performance and our column physical_locations
is a type of geography
we can directly create an index on this column but If we need to query on geometry
type we need to create a spatial index by casting into geometry
type.
defmodule Data.Repo.Migrations.CreateServiceProvidersPhysicalLocationsGeographyIndex do
@doc """
`physical_location_geopoints` is type of `geography` and creating index for
`geography` type column.
Reference: [Spatial Index](http://postgis.net/workshops/postgis-intro/indexing.html)
"""
use Ecto.Migration
@disable_ddl_transaction true
@disable_migration_lock true
def up do
execute """
CREATE INDEX CONCURRENTLY IF NOT EXISTS service_providers_physical_location_geopoints_geography_index
ON providers ( physical_location_geopoints )
"""
end
def down do
execute """
DROP INDEX IF EXISTS service_providers_physical_location_geopoints_geography_index
"""
end
end
💡 In PostgreSQL, Index name should be under 64 characters, otherwise remaining characters will be truncated.
Last updated