First I want to show you the current database structure. There are three tables:
dishes (id, name)
locations (id, name, coordinates (POINT))
dish_location(location_id, dish_id)
Now I want to implement an API which gets the position (latitude, longitude) of the user and returns a list of dishes sorted by the distance in km. I already have a method which takes two latitudes and two longitudes and gives me the distance. But I am sure you can show me a way, which is a more performant way to do this directly in the MySQL query.
Additional: I want to do a "load more"-function in the API. So I pass the count of already received items or how would I solve this in this case?
I am using this for the MySQL Spatial package
First, let's take a look at how to do this with the basic query builder. Then, we'll discuss how to execute this query with Eloquent models:
function paginateDishesFromPoint(Point $point, $pageSize)
{
$distanceField = "ST_Distance_Sphere(locations.coordinates, "
. "ST_GeomFromText('{$point->toWKT()}') AS distance";
return DB::table('dishes')
->select('dishes.*', DB::raw($distanceField))
->join('dish_locations', 'dish_locations.dish_id', '=', 'dishes.id')
->join('locations', 'locations.id', '=', 'dish_locations.location_id')
->orderBy('distance')
->paginate($pageSize);
}
The ST_Distance_Sphere()
function calculates a distance that we can sort results by. Laravel's paginate()
method performs automatic pagination for us using the page
parameter passed through the request URL. Read the pagination docs for more information. With the function above, we can fetch a paginated result set as follows:
$point = new Point($latitude, $longitude);
$sortedDishes = paginateDishesFromPoint($point, 15);
...where Point
is the Grimzy\LaravelMysqlSpatial\Types\Point
class from the package we're using, and 15
is the number of results per page.
Now, let's try to do this with Eloquent models. We'll use a local query scope to encapsulate the logic needed to create the portion of the query that performs the ordering:
class Dish extends Model
{
...
public function locations()
{
return $this->belongsToMany(App\Location::class);
}
public function scopeOrderByDistanceFrom($query, Point $point)
{
$relation = $this->locations();
$locationsTable = $relation->getRelated()->getTable();
$distanceField = "ST_Distance_Sphere($locationsTable.coordinates, "
. "ST_GeomFromText('{$point->toWKT()}') AS distance";
return $query
->select($this->getTable() . '.*', DB::raw($distanceField))
->join(
$relation->getTable(),
$relation->getQualifiedForeignKeyName(),
'=',
$relation->getQualifiedParentKeyName()
)
->join(
$locationsTable,
$relation->getRelated()->getQualifiedKeyName(),
'=',
$relation->getQualifiedRelatedKeyName()
)
->orderBy('distance');
}
}
This implementation uses metadata on the models to add the table and field names to the query so we don't need to update this method if they change. Now we can fetch the ordered set using the model:
$point = new Point($latitude, $longitude);
$sortedDishes = Dish::orderByDistanceFrom($point)->paginate($pageSize);
$sortedDishes
is an instance of Laravel's LengthAwarePaginator
which wraps a Collection
of the models. If we pass the results to a view, here's how to display them in a Blade template:
<ul>
@foreach($sortedDishes as $dish)
<li>{{ $dish->name }} is {{ $dish->distance }} meters away.</li>
@endforeach
</ul>
<a href="{{ $sortedDishes->nextPageUrl() }}">Load more...</a>
As shown above, the paginator provides convenience methods that we can use to easily move between paged results.
Alternatively, we could use AJAX requests to load the results. Just be sure to pass the current page + 1 in the page
parameter of the request data.
SQL
SELECT d.name AS `Dish`,
l.name AS `Location`,
ST_Distance(l.coordinates, POINT(<<<longitude>>>, <<<latitude>>>)) AS `Distance`
FROM dish_locations dl
JOIN dishes d
ON d.id = dl.dish_id
JOIN locations l
ON l.id = dl.location_id
ORDER BY `Distance`
LIMIT <<<n-1>>>, <<<page size>>>;
...inserting <<<longitude>>>
and <<<latitude>>>
from the user's position, <<<n-1>>>
from the number of rows that have already been retrieved minus one and <<<page size>>>
as the desired next number of rows to retrieve.
Demo
http://rextester.com/YAEBF16430
Explanation
ST_Distance_Sphere
is used to calculate the distance in meters.LIMIT <<offset>>, <<num>>
is used to implement the paging (where offset is zero-based).Postgis has two operators (<-> and <#>) to work distance and calculate KNN(Nearest Neighbors). You can use them instead of st_distance. Operators Query would be something like:
WITH index_query AS (
SELECT ST_Distance(geom, 'SRID=3005;POINT(1011102 450541)'::geometry) as d,edabbr, vaabbr
FROM va2005
ORDER BY geom <-> 'SRID=3005;POINT(1011102 450541)'::geometry LIMIT 100)
SELECT *
FROM index_query
ORDER BY d limit 10;
Using distance as d you can order.
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