Skip to content

Commit 11972fd

Browse files
daveadracos
authored andcommitted
Improve performance of /nearest.
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.
1 parent db2322f commit 11972fd

2 files changed

Lines changed: 46 additions & 4 deletions

File tree

mapit/tests/test_views.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,9 +114,22 @@ def test_nearest_with_bad_srid(self):
114114
self.assertEqual(response.status_code, 400)
115115
content = json.loads(response.content.decode('utf-8'))
116116
self.assertEqual(content, {
117-
'code': 400, 'error': 'GetProj4StringSPI: Cannot find SRID (84) in spatial_ref_sys\n'
117+
'code': 400, 'error': 'Point outside the area geometry'
118118
})
119119

120+
def test_nearest(self):
121+
url = '/nearest/4326/4,52.json'
122+
response = self.client.get(url)
123+
content = get_content(response)
124+
self.assertEqual(content, {'postcode': {
125+
'coordsyst': 'G',
126+
'distance': 519051.0,
127+
'easting': 295977,
128+
'northing': 178963,
129+
'postcode': 'PO14 1NT',
130+
'wgs84_lat': 51.5, 'wgs84_lon': -3.5
131+
}})
132+
120133
def test_areas_polygon_valid(self):
121134
id1 = self.small_area_1.id
122135
id2 = self.small_area_2.id

mapit/views/postcodes.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,17 @@
1+
from operator import attrgetter
12
import re
23
import itertools
34
from django.db.utils import DatabaseError
45

6+
from django.utils.translation import ugettext as _
57
from django.shortcuts import redirect, render
68
from django.contrib.gis.geos import Point
7-
from django.contrib.gis.measure import D
9+
from django.contrib.gis.geometry.backend import Geometry
810
from django.contrib.gis.db.models import Collect
911
from django.views.decorators.csrf import csrf_exempt
12+
from django.db.models import FloatField
13+
from django.db.models.expressions import Func
14+
from django.contrib.gis.db.backends.postgis.adapter import PostGISAdapter
1015

1116
from mapit.models import Postcode, Area, Generation
1217
from mapit.utils import is_valid_postcode, is_valid_partial_postcode
@@ -36,6 +41,19 @@
3641
}
3742

3843

44+
class GeometryCentroidDistance(Func):
45+
function = ''
46+
arg_joiner = ' <-> '
47+
48+
def __init__(self, expression, geom, **extra):
49+
if not isinstance(geom, Geometry):
50+
raise TypeError("Please provide a geometry object.")
51+
if not hasattr(geom, 'srid') or not geom.srid:
52+
raise ValueError("Please provide a geometry attribute with a defined SRID.")
53+
super(GeometryCentroidDistance, self).__init__(
54+
expression, PostGISAdapter(geom), output_field=FloatField(), **extra)
55+
56+
3957
@ratelimit(minutes=3, requests=100)
4058
def postcode(request, postcode, format=None):
4159
if hasattr(countries, 'canonical_postcode'):
@@ -155,9 +173,20 @@ def form_submitted(request):
155173
def nearest(request, srid, x, y, format='json'):
156174
location = Point(float(x), float(y), srid=int(srid))
157175
set_timeout(format)
176+
177+
try:
178+
# Transform to database SRID for comparison (GeometryCentroidDistance does not yet do this)
179+
location.transform(4326)
180+
except:
181+
raise ViewException(format, _('Point outside the area geometry'), 400)
182+
158183
try:
159-
postcode = Postcode.objects.filter(
160-
location__distance_gte=(location, D(mi=0))).distance(location).order_by('distance')[0]
184+
# Ordering will be in 'degrees', so fetch a few and sort by actual distance
185+
postcodes = Postcode.objects.annotate(
186+
centroid_distance=GeometryCentroidDistance('location', location)
187+
).distance(location).order_by('centroid_distance')[:100]
188+
postcodes = sorted(postcodes, key=attrgetter('distance'))
189+
postcode = postcodes[0]
161190
except DatabaseError as e:
162191
if 'Cannot find SRID' in e.args[0]:
163192
raise ViewException(format, e.args[0], 400)

0 commit comments

Comments
 (0)