Skip to content

Commit

Permalink
Improve performance of /nearest.
Browse files Browse the repository at this point in the history
The nearest postcode lookup performance reduced dramatically when tried
with Postgres 9.6 and PostGIS 2.3. This commit updates the query to use
the ‘<->’ centroid distance operator instead, which makes better use of
the geoindex and is much faster to execute. We fetch a few rows ordered
by this, then sort by the actual distance, as the operator is returning
'degree' distances because postcodes are currently geometry objects.

Handily, the work @dracos did to introduce the TrigramDistance function
into Django 1.10 was easily adaptable for this purpose, as the operator
used is the same.
  • Loading branch information
davea authored and dracos committed Oct 25, 2017
1 parent db2322f commit 11972fd
Show file tree
Hide file tree
Showing 2 changed files with 46 additions and 4 deletions.
15 changes: 14 additions & 1 deletion mapit/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,22 @@ def test_nearest_with_bad_srid(self):
self.assertEqual(response.status_code, 400)
content = json.loads(response.content.decode('utf-8'))
self.assertEqual(content, {
'code': 400, 'error': 'GetProj4StringSPI: Cannot find SRID (84) in spatial_ref_sys\n'
'code': 400, 'error': 'Point outside the area geometry'
})

def test_nearest(self):
url = '/nearest/4326/4,52.json'
response = self.client.get(url)
content = get_content(response)
self.assertEqual(content, {'postcode': {
'coordsyst': 'G',
'distance': 519051.0,
'easting': 295977,
'northing': 178963,
'postcode': 'PO14 1NT',
'wgs84_lat': 51.5, 'wgs84_lon': -3.5
}})

def test_areas_polygon_valid(self):
id1 = self.small_area_1.id
id2 = self.small_area_2.id
Expand Down
35 changes: 32 additions & 3 deletions mapit/views/postcodes.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
from operator import attrgetter
import re
import itertools
from django.db.utils import DatabaseError

from django.utils.translation import ugettext as _
from django.shortcuts import redirect, render
from django.contrib.gis.geos import Point
from django.contrib.gis.measure import D
from django.contrib.gis.geometry.backend import Geometry
from django.contrib.gis.db.models import Collect
from django.views.decorators.csrf import csrf_exempt
from django.db.models import FloatField
from django.db.models.expressions import Func
from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter

from mapit.models import Postcode, Area, Generation
from mapit.utils import is_valid_postcode, is_valid_partial_postcode
Expand Down Expand Up @@ -36,6 +41,19 @@
}


class GeometryCentroidDistance(Func):
function = ''
arg_joiner = ' <-> '

def __init__(self, expression, geom, **extra):
if not isinstance(geom, Geometry):
raise TypeError("Please provide a geometry object.")
if not hasattr(geom, 'srid') or not geom.srid:
raise ValueError("Please provide a geometry attribute with a defined SRID.")
super(GeometryCentroidDistance, self).__init__(
expression, PostGISAdapter(geom), output_field=FloatField(), **extra)


@ratelimit(minutes=3, requests=100)
def postcode(request, postcode, format=None):
if hasattr(countries, 'canonical_postcode'):
Expand Down Expand Up @@ -155,9 +173,20 @@ def form_submitted(request):
def nearest(request, srid, x, y, format='json'):
location = Point(float(x), float(y), srid=int(srid))
set_timeout(format)

try:
# Transform to database SRID for comparison (GeometryCentroidDistance does not yet do this)
location.transform(4326)
except:
raise ViewException(format, _('Point outside the area geometry'), 400)

try:
postcode = Postcode.objects.filter(
location__distance_gte=(location, D(mi=0))).distance(location).order_by('distance')[0]
# Ordering will be in 'degrees', so fetch a few and sort by actual distance
postcodes = Postcode.objects.annotate(
centroid_distance=GeometryCentroidDistance('location', location)
).distance(location).order_by('centroid_distance')[:100]
postcodes = sorted(postcodes, key=attrgetter('distance'))
postcode = postcodes[0]
except DatabaseError as e:
if 'Cannot find SRID' in e.args[0]:
raise ViewException(format, e.args[0], 400)
Expand Down

0 comments on commit 11972fd

Please sign in to comment.