Skip to content

Commit

Permalink
Merge branch 'master' of github.com:barseghyanartur/django-elasticsea…
Browse files Browse the repository at this point in the history
…rch-dsl-drf
  • Loading branch information
barseghyanartur committed Oct 14, 2020
2 parents 0a65b83 + c65eec5 commit e20de01
Show file tree
Hide file tree
Showing 8 changed files with 469 additions and 1 deletion.
85 changes: 85 additions & 0 deletions docs/advanced_usage_examples.rst
Original file line number Diff line number Diff line change
Expand Up @@ -927,6 +927,91 @@ Ordering
http://localhost:8000/search/publishers/?ordering=location__48.85__2.30__km__plane
Geo-shape
~~~~~~~~

**Setup**

In order to be able to do all geo-shape queries, you need a GeoShapeField with 'recursive' strategy.
Details about spatial strategies here : https://www.elastic.co/guide/en/elasticsearch/reference/master/geo-shape.html#spatial-strategy

.. code-block:: python
# ...
@INDEX.doc_type
class PublisherDocument(Document):
# ...
location_circle = fields.GeoShapeField(strategy='recursive',
attr='location_circle_indexing')
# ...
class Publisher(models.Model):
# ...
@property
def location_circle_indexing(self):
"""
Indexing circle geo_shape with 10km radius.
Used in Elasticsearch indexing/tests of `geo_shape` native filter.
"""
return {
'type': 'circle',
'coordinates': [self.latitude, self.longitude],
'radius': '10km',
}
You need to use GeoSpatialFilteringFilterBackend and set the LOOKUP_FILTER_GEO_SHAPE to the geo_spatial_filter_field. (This takes place in ViewSet)

.. code-block:: python
# ...
class PublisherDocumentViewSet(DocumentViewSet):
# ...
filter_backends = [
# ...
GeoSpatialFilteringFilterBackend,
# ...
]
# ...
geo_spatial_filter_fields = {
# ...
'location_circle': {
'lookups': [
LOOKUP_FILTER_GEO_SHAPE,
]
},
# ...
}
# ...
**Supported shapes & queries**

With this setup, we can do several types of Geo-shape queries.

Supported and tested shapes types are : point, circle, envelope

Pottentially supported but untested shapes are : multipoint and linestring

Supported and tested queries are : INTERSECTS, DISJOINT, WITHIN, CONTAINS

**Shapes intersects**

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)

.. code-block:: text
http://localhost:8000/search/publishers/?location_circle__geo_shape=49.119696,6.176355__radius,15km__relation,intersects__type,circle
This request give you all publishers having a location_circle intersecting with the one in the query.


Suggestions
-----------

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

import factories


DEFAULT_NUMBER_OF_ITEMS_TO_CREATE = 100


Expand Down Expand Up @@ -54,3 +53,16 @@ def handle(self, *args, **options):
print("{} address objects created.".format(number))
except Exception as err:
raise CommandError(str(err))

try:
points = [[50.691589, 3.174173], [49.076088, 6.222905], [48.983755, 6.019749]]
for point in points:
factories.PublisherFactory.create(
**{
'latitude': point[0],
'longitude': point[1],
}
)
print("{} publishers objects created.".format(len(points)))
except Exception as err:
raise CommandError(str(err))
23 changes: 23 additions & 0 deletions examples/simple/books/models/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,26 @@ def location_field_indexing(self):
'lat': self.latitude,
'lon': self.longitude,
}

@property
def location_point_indexing(self):
"""
Indexing point geo_shape.
Used in Elasticsearch indexing/tests of `geo_shape` native filter.
"""
return {
'type': 'point',
'coordinates': [self.latitude, self.longitude],
}

@property
def location_circle_indexing(self):
"""
Indexing circle geo_shape with 10km radius.
Used in Elasticsearch indexing/tests of `geo_shape` native filter.
"""
return {
'type': 'circle',
'coordinates': [self.latitude, self.longitude],
'radius': '10km',
}
6 changes: 6 additions & 0 deletions examples/simple/search_indexes/documents/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ class PublisherDocument(Document):
# Location
location = fields.GeoPointField(attr='location_field_indexing')

# Geo-shape fields
location_point = fields.GeoShapeField(strategy='recursive',
attr='location_point_indexing')
location_circle = fields.GeoShapeField(strategy='recursive',
attr='location_circle_indexing')

class Django(object):
model = Publisher # The model associate with this Document

Expand Down
11 changes: 11 additions & 0 deletions examples/simple/search_indexes/viewsets/publisher.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
LOOKUP_FILTER_GEO_BOUNDING_BOX,
LOOKUP_FILTER_GEO_DISTANCE,
LOOKUP_FILTER_GEO_POLYGON,
LOOKUP_FILTER_GEO_SHAPE,
SUGGESTER_COMPLETION,
SUGGESTER_PHRASE,
SUGGESTER_TERM,
Expand Down Expand Up @@ -73,6 +74,16 @@ class PublisherDocumentViewSet(DocumentViewSet):
],
},
'location_2': 'location',
'location_point': {
'lookups': [
LOOKUP_FILTER_GEO_SHAPE,
]
},
'location_circle': {
'lookups': [
LOOKUP_FILTER_GEO_SHAPE,
]
},
}
# Define ordering fields
ordering_fields = {
Expand Down
39 changes: 39 additions & 0 deletions src/django_elasticsearch_dsl_drf/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
'LOOKUP_FILTER_GEO_BOUNDING_BOX',
'LOOKUP_FILTER_GEO_DISTANCE',
'LOOKUP_FILTER_GEO_POLYGON',
'LOOKUP_FILTER_GEO_SHAPE',
'LOOKUP_FILTER_PREFIX',
'LOOKUP_FILTER_RANGE',
'LOOKUP_FILTER_REGEXP',
Expand Down Expand Up @@ -321,6 +322,44 @@
# /api/articles/?location__geo_bounding_box=40.73,-74.1__40.01,-71.12
LOOKUP_FILTER_GEO_BOUNDING_BOX = 'geo_bounding_box'

# Geo Shape Query
# Finds documents with:
# - geo-shapes which either intersect, are contained by, or do not intersect with the specified geo-shape
# - geo-points which intersect the specified geo-shape
#
# Example:
# {
# "query": {
# "bool" : {
# "must" : {
# "match_all" : {}
# },
# "filter": {
# "geo_shape": {
# "location": {
# "shape": {
# "type": "circle",
# "coordinates": [48.9864453, 6.37977],
# "radius": "20km"
# },
# "relation": "intersects"
# }
# }
# }
# }
# }
# }
#
# Query options:
#
# - type: Shape type (envelope, circle, polygon, ...)
# - relation: Spatial relation operator (intersects, disjoint, within, ...)
# - radius: in case of circle type, represents circle radius
#
# Example: http://localhost:8000
# /api/articles/?location__geo_shape=48.9864453,6.37977__relation,intersects__type,circle__radius,20km
LOOKUP_FILTER_GEO_SHAPE = 'geo_shape'

# ****************************************************************************
# ************************ Functional filters/queries ************************
# ****************************************************************************
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
LOOKUP_FILTER_GEO_DISTANCE,
LOOKUP_FILTER_GEO_POLYGON,
LOOKUP_FILTER_GEO_BOUNDING_BOX,
LOOKUP_FILTER_GEO_SHAPE,
SEPARATOR_LOOKUP_COMPLEX_VALUE,
SEPARATOR_LOOKUP_COMPLEX_MULTIPLE_VALUE,
)
Expand Down Expand Up @@ -340,6 +341,112 @@ def get_geo_bounding_box_params(cls, value, field):

return params

@classmethod
def get_geo_shape_params(cls, value, field):
"""Get params for `geo_shape` query.
Example:
/search/publishers/?location__geo_shape=48.9864453,6.37977
__relation,intersects
__type,circle
__radius,20km
Example:
/search/publishers/?location__geo_shape=48.906254,6.378593
__48.985850,6.479359
__relation,within
__type,envelope
Elasticsearch:
{
"query": {
"bool" : {
"must" : {
"match_all" : {}
},
"filter" : {
"geo_shape" : {
"location" : {
"shape": {
"type": "circle",
"coordinates": [48.9864453, 6.37977],
"radius": "20km"
},
"relation": "intersects"
}
}
}
}
}
}
:param value:
:param field:
:type value: str
:type field:
:return: Params to be used in `geo_shape` query.
:rtype: dict
"""
__values = cls.split_lookup_complex_value(value)
__len_values = len(__values)

if not __len_values:
return {}

__coordinates = []
__options = {}

# Parse coordinates (can be x points)
for value in __values:
__lat_lon = value.split(
SEPARATOR_LOOKUP_COMPLEX_MULTIPLE_VALUE
)
if len(__lat_lon) >= 2:
try:
__point = [
float(__lat_lon[0]),
float(__lat_lon[1]),
]
__coordinates.append(list(__point))
except ValueError:
if SEPARATOR_LOOKUP_COMPLEX_MULTIPLE_VALUE in value:
__opt_name_val = value.split(
SEPARATOR_LOOKUP_COMPLEX_MULTIPLE_VALUE
)
if len(__opt_name_val) >= 2:
if __opt_name_val[0] in ('relation',
'type',
'radius'):
__options.update(
{
__opt_name_val[0]: __opt_name_val[1]
}
)

__type = __options.pop('type', None)
__relation = __options.pop('relation', None)
if not __coordinates or not __type or not __relation:
return {}

params = {
field: {
'shape': {
'type': __type,
'coordinates': __coordinates if len(__coordinates) > 1 else __coordinates[0],
},
'relation': __relation,
}
}
radius = __options.pop('radius', None)
if radius:
params[field]['shape'].update({'radius': radius})
params.update(__options)

return params

@classmethod
def apply_query_geo_distance(cls, queryset, options, value):
"""Apply `geo_distance` query.
Expand Down Expand Up @@ -400,6 +507,26 @@ def apply_query_geo_bounding_box(cls, queryset, options, value):
)
)

@classmethod
def apply_query_geo_shape(cls, queryset, options, value):
"""Apply `geo_shape` query.
:param queryset: Original queryset.
:param options: Filter options.
:param value: value to filter on.
:type queryset: elasticsearch_dsl.search.Search
:type options: dict
:type value: str
:return: Modified queryset.
:rtype: elasticsearch_dsl.search.Search
"""
return queryset.query(
Q(
'geo_shape',
**cls.get_geo_shape_params(value, options['field'])
)
)

def get_filter_query_params(self, request, view):
"""Get query params to be filtered on.
Expand Down Expand Up @@ -491,4 +618,12 @@ def filter_queryset(self, request, queryset, view):
value
)

# `geo_shape` query lookup
elif options['lookup'] == LOOKUP_FILTER_GEO_SHAPE:
queryset = self.apply_query_geo_shape(
queryset,
options,
value
)

return queryset
Loading

0 comments on commit e20de01

Please sign in to comment.