Skip to content

Commit e20de01

Browse files
Merge branch 'master' of github.com:barseghyanartur/django-elasticsearch-dsl-drf
2 parents 0a65b83 + c65eec5 commit e20de01

File tree

8 files changed

+469
-1
lines changed

8 files changed

+469
-1
lines changed

docs/advanced_usage_examples.rst

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,6 +927,91 @@ Ordering
927927
928928
http://localhost:8000/search/publishers/?ordering=location__48.85__2.30__km__plane
929929
930+
Geo-shape
931+
~~~~~~~~
932+
933+
**Setup**
934+
935+
In order to be able to do all geo-shape queries, you need a GeoShapeField with 'recursive' strategy.
936+
Details about spatial strategies here : https://www.elastic.co/guide/en/elasticsearch/reference/master/geo-shape.html#spatial-strategy
937+
938+
.. code-block:: python
939+
940+
# ...
941+
942+
@INDEX.doc_type
943+
class PublisherDocument(Document):
944+
945+
# ...
946+
947+
location_circle = fields.GeoShapeField(strategy='recursive',
948+
attr='location_circle_indexing')
949+
950+
# ...
951+
952+
class Publisher(models.Model):
953+
954+
# ...
955+
956+
@property
957+
def location_circle_indexing(self):
958+
"""
959+
Indexing circle geo_shape with 10km radius.
960+
Used in Elasticsearch indexing/tests of `geo_shape` native filter.
961+
"""
962+
return {
963+
'type': 'circle',
964+
'coordinates': [self.latitude, self.longitude],
965+
'radius': '10km',
966+
}
967+
968+
969+
You need to use GeoSpatialFilteringFilterBackend and set the LOOKUP_FILTER_GEO_SHAPE to the geo_spatial_filter_field. (This takes place in ViewSet)
970+
971+
.. code-block:: python
972+
973+
# ...
974+
class PublisherDocumentViewSet(DocumentViewSet):
975+
# ...
976+
filter_backends = [
977+
# ...
978+
GeoSpatialFilteringFilterBackend,
979+
# ...
980+
]
981+
# ...
982+
geo_spatial_filter_fields = {
983+
# ...
984+
'location_circle': {
985+
'lookups': [
986+
LOOKUP_FILTER_GEO_SHAPE,
987+
]
988+
},
989+
# ...
990+
}
991+
# ...
992+
993+
994+
**Supported shapes & queries**
995+
996+
With this setup, we can do several types of Geo-shape queries.
997+
998+
Supported and tested shapes types are : point, circle, envelope
999+
1000+
Pottentially supported but untested shapes are : multipoint and linestring
1001+
1002+
Supported and tested queries are : INTERSECTS, DISJOINT, WITHIN, CONTAINS
1003+
1004+
**Shapes intersects**
1005+
1006+
Interesting queries are shape intersects : this gives you all documents whose shape intersects with the shape given in query. (Should be 2 with the actual test dataset)
1007+
1008+
.. code-block:: text
1009+
1010+
http://localhost:8000/search/publishers/?location_circle__geo_shape=49.119696,6.176355__radius,15km__relation,intersects__type,circle
1011+
1012+
This request give you all publishers having a location_circle intersecting with the one in the query.
1013+
1014+
9301015
Suggestions
9311016
-----------
9321017

examples/simple/books/management/commands/books_create_test_data.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
import factories
66

7-
87
DEFAULT_NUMBER_OF_ITEMS_TO_CREATE = 100
98

109

@@ -54,3 +53,16 @@ def handle(self, *args, **options):
5453
print("{} address objects created.".format(number))
5554
except Exception as err:
5655
raise CommandError(str(err))
56+
57+
try:
58+
points = [[50.691589, 3.174173], [49.076088, 6.222905], [48.983755, 6.019749]]
59+
for point in points:
60+
factories.PublisherFactory.create(
61+
**{
62+
'latitude': point[0],
63+
'longitude': point[1],
64+
}
65+
)
66+
print("{} publishers objects created.".format(len(points)))
67+
except Exception as err:
68+
raise CommandError(str(err))

examples/simple/books/models/publisher.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,26 @@ def location_field_indexing(self):
4949
'lat': self.latitude,
5050
'lon': self.longitude,
5151
}
52+
53+
@property
54+
def location_point_indexing(self):
55+
"""
56+
Indexing point geo_shape.
57+
Used in Elasticsearch indexing/tests of `geo_shape` native filter.
58+
"""
59+
return {
60+
'type': 'point',
61+
'coordinates': [self.latitude, self.longitude],
62+
}
63+
64+
@property
65+
def location_circle_indexing(self):
66+
"""
67+
Indexing circle geo_shape with 10km radius.
68+
Used in Elasticsearch indexing/tests of `geo_shape` native filter.
69+
"""
70+
return {
71+
'type': 'circle',
72+
'coordinates': [self.latitude, self.longitude],
73+
'radius': '10km',
74+
}

examples/simple/search_indexes/documents/publisher.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,12 @@ class PublisherDocument(Document):
7878
# Location
7979
location = fields.GeoPointField(attr='location_field_indexing')
8080

81+
# Geo-shape fields
82+
location_point = fields.GeoShapeField(strategy='recursive',
83+
attr='location_point_indexing')
84+
location_circle = fields.GeoShapeField(strategy='recursive',
85+
attr='location_circle_indexing')
86+
8187
class Django(object):
8288
model = Publisher # The model associate with this Document
8389

examples/simple/search_indexes/viewsets/publisher.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
LOOKUP_FILTER_GEO_BOUNDING_BOX,
55
LOOKUP_FILTER_GEO_DISTANCE,
66
LOOKUP_FILTER_GEO_POLYGON,
7+
LOOKUP_FILTER_GEO_SHAPE,
78
SUGGESTER_COMPLETION,
89
SUGGESTER_PHRASE,
910
SUGGESTER_TERM,
@@ -73,6 +74,16 @@ class PublisherDocumentViewSet(DocumentViewSet):
7374
],
7475
},
7576
'location_2': 'location',
77+
'location_point': {
78+
'lookups': [
79+
LOOKUP_FILTER_GEO_SHAPE,
80+
]
81+
},
82+
'location_circle': {
83+
'lookups': [
84+
LOOKUP_FILTER_GEO_SHAPE,
85+
]
86+
},
7687
}
7788
# Define ordering fields
7889
ordering_fields = {

src/django_elasticsearch_dsl_drf/constants.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
'LOOKUP_FILTER_GEO_BOUNDING_BOX',
2626
'LOOKUP_FILTER_GEO_DISTANCE',
2727
'LOOKUP_FILTER_GEO_POLYGON',
28+
'LOOKUP_FILTER_GEO_SHAPE',
2829
'LOOKUP_FILTER_PREFIX',
2930
'LOOKUP_FILTER_RANGE',
3031
'LOOKUP_FILTER_REGEXP',
@@ -321,6 +322,44 @@
321322
# /api/articles/?location__geo_bounding_box=40.73,-74.1__40.01,-71.12
322323
LOOKUP_FILTER_GEO_BOUNDING_BOX = 'geo_bounding_box'
323324

325+
# Geo Shape Query
326+
# Finds documents with:
327+
# - geo-shapes which either intersect, are contained by, or do not intersect with the specified geo-shape
328+
# - geo-points which intersect the specified geo-shape
329+
#
330+
# Example:
331+
# {
332+
# "query": {
333+
# "bool" : {
334+
# "must" : {
335+
# "match_all" : {}
336+
# },
337+
# "filter": {
338+
# "geo_shape": {
339+
# "location": {
340+
# "shape": {
341+
# "type": "circle",
342+
# "coordinates": [48.9864453, 6.37977],
343+
# "radius": "20km"
344+
# },
345+
# "relation": "intersects"
346+
# }
347+
# }
348+
# }
349+
# }
350+
# }
351+
# }
352+
#
353+
# Query options:
354+
#
355+
# - type: Shape type (envelope, circle, polygon, ...)
356+
# - relation: Spatial relation operator (intersects, disjoint, within, ...)
357+
# - radius: in case of circle type, represents circle radius
358+
#
359+
# Example: http://localhost:8000
360+
# /api/articles/?location__geo_shape=48.9864453,6.37977__relation,intersects__type,circle__radius,20km
361+
LOOKUP_FILTER_GEO_SHAPE = 'geo_shape'
362+
324363
# ****************************************************************************
325364
# ************************ Functional filters/queries ************************
326365
# ****************************************************************************

src/django_elasticsearch_dsl_drf/filter_backends/filtering/geo_spatial.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
LOOKUP_FILTER_GEO_DISTANCE,
3333
LOOKUP_FILTER_GEO_POLYGON,
3434
LOOKUP_FILTER_GEO_BOUNDING_BOX,
35+
LOOKUP_FILTER_GEO_SHAPE,
3536
SEPARATOR_LOOKUP_COMPLEX_VALUE,
3637
SEPARATOR_LOOKUP_COMPLEX_MULTIPLE_VALUE,
3738
)
@@ -340,6 +341,112 @@ def get_geo_bounding_box_params(cls, value, field):
340341

341342
return params
342343

344+
@classmethod
345+
def get_geo_shape_params(cls, value, field):
346+
"""Get params for `geo_shape` query.
347+
348+
Example:
349+
350+
/search/publishers/?location__geo_shape=48.9864453,6.37977
351+
__relation,intersects
352+
__type,circle
353+
__radius,20km
354+
355+
Example:
356+
357+
/search/publishers/?location__geo_shape=48.906254,6.378593
358+
__48.985850,6.479359
359+
__relation,within
360+
__type,envelope
361+
362+
Elasticsearch:
363+
364+
{
365+
"query": {
366+
"bool" : {
367+
"must" : {
368+
"match_all" : {}
369+
},
370+
"filter" : {
371+
"geo_shape" : {
372+
"location" : {
373+
"shape": {
374+
"type": "circle",
375+
"coordinates": [48.9864453, 6.37977],
376+
"radius": "20km"
377+
},
378+
"relation": "intersects"
379+
}
380+
}
381+
}
382+
}
383+
}
384+
}
385+
386+
:param value:
387+
:param field:
388+
:type value: str
389+
:type field:
390+
:return: Params to be used in `geo_shape` query.
391+
:rtype: dict
392+
"""
393+
__values = cls.split_lookup_complex_value(value)
394+
__len_values = len(__values)
395+
396+
if not __len_values:
397+
return {}
398+
399+
__coordinates = []
400+
__options = {}
401+
402+
# Parse coordinates (can be x points)
403+
for value in __values:
404+
__lat_lon = value.split(
405+
SEPARATOR_LOOKUP_COMPLEX_MULTIPLE_VALUE
406+
)
407+
if len(__lat_lon) >= 2:
408+
try:
409+
__point = [
410+
float(__lat_lon[0]),
411+
float(__lat_lon[1]),
412+
]
413+
__coordinates.append(list(__point))
414+
except ValueError:
415+
if SEPARATOR_LOOKUP_COMPLEX_MULTIPLE_VALUE in value:
416+
__opt_name_val = value.split(
417+
SEPARATOR_LOOKUP_COMPLEX_MULTIPLE_VALUE
418+
)
419+
if len(__opt_name_val) >= 2:
420+
if __opt_name_val[0] in ('relation',
421+
'type',
422+
'radius'):
423+
__options.update(
424+
{
425+
__opt_name_val[0]: __opt_name_val[1]
426+
}
427+
)
428+
429+
__type = __options.pop('type', None)
430+
__relation = __options.pop('relation', None)
431+
if not __coordinates or not __type or not __relation:
432+
return {}
433+
434+
params = {
435+
field: {
436+
'shape': {
437+
'type': __type,
438+
'coordinates': __coordinates if len(__coordinates) > 1 else __coordinates[0],
439+
},
440+
'relation': __relation,
441+
}
442+
}
443+
radius = __options.pop('radius', None)
444+
if radius:
445+
params[field]['shape'].update({'radius': radius})
446+
params.update(__options)
447+
448+
return params
449+
343450
@classmethod
344451
def apply_query_geo_distance(cls, queryset, options, value):
345452
"""Apply `geo_distance` query.
@@ -400,6 +507,26 @@ def apply_query_geo_bounding_box(cls, queryset, options, value):
400507
)
401508
)
402509

510+
@classmethod
511+
def apply_query_geo_shape(cls, queryset, options, value):
512+
"""Apply `geo_shape` query.
513+
514+
:param queryset: Original queryset.
515+
:param options: Filter options.
516+
:param value: value to filter on.
517+
:type queryset: elasticsearch_dsl.search.Search
518+
:type options: dict
519+
:type value: str
520+
:return: Modified queryset.
521+
:rtype: elasticsearch_dsl.search.Search
522+
"""
523+
return queryset.query(
524+
Q(
525+
'geo_shape',
526+
**cls.get_geo_shape_params(value, options['field'])
527+
)
528+
)
529+
403530
def get_filter_query_params(self, request, view):
404531
"""Get query params to be filtered on.
405532
@@ -491,4 +618,12 @@ def filter_queryset(self, request, queryset, view):
491618
value
492619
)
493620

621+
# `geo_shape` query lookup
622+
elif options['lookup'] == LOOKUP_FILTER_GEO_SHAPE:
623+
queryset = self.apply_query_geo_shape(
624+
queryset,
625+
options,
626+
value
627+
)
628+
494629
return queryset

0 commit comments

Comments
 (0)