Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GEOSEARCH and GEOSEARCHSTORE #1526

Merged
merged 8 commits into from
Sep 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions redis/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -433,18 +433,23 @@ def parse_cluster_nodes(response, **options):
return dict(_parse_node_line(line) for line in raw_lines)


def parse_georadius_generic(response, **options):
AvitalFineRedis marked this conversation as resolved.
Show resolved Hide resolved
def parse_geosearch_generic(response, **options):
"""
Parse the response of 'GEOSEARCH', GEORADIUS' and 'GEORADIUSBYMEMBER'
commands according to 'withdist', 'withhash' and 'withcoord' labels.
"""
if options['store'] or options['store_dist']:
# `store` and `store_diff` cant be combined
# `store` and `store_dist` cant be combined
# with other command arguments.
# relevant to 'GEORADIUS' and 'GEORADIUSBYMEMBER'
return response

if type(response) != list:
response_list = [response]
else:
response_list = response

if not options['withdist'] and not options['withcoord']\
if not options['withdist'] and not options['withcoord'] \
and not options['withhash']:
# just a bunch of places
return response_list
Expand Down Expand Up @@ -654,8 +659,9 @@ class Redis(Commands, object):
'GEOPOS': lambda r: list(map(lambda ll: (float(ll[0]),
float(ll[1]))
if ll is not None else None, r)),
'GEORADIUS': parse_georadius_generic,
'GEORADIUSBYMEMBER': parse_georadius_generic,
'GEOSEARCH': parse_geosearch_generic,
'GEORADIUS': parse_geosearch_generic,
'GEORADIUSBYMEMBER': parse_geosearch_generic,
'HGETALL': lambda r: r and pairs_to_dict(r) or {},
'HSCAN': parse_hscan,
'INFO': parse_info,
Expand Down
130 changes: 130 additions & 0 deletions redis/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -2795,6 +2795,136 @@ def _georadiusgeneric(self, command, *args, **kwargs):

return self.execute_command(command, *pieces, **kwargs)

def geosearch(self, name, member=None, longitude=None, latitude=None,
unit='m', radius=None, width=None, height=None, sort=None,
count=None, any=False, withcoord=False,
withdist=False, withhash=False):
"""
Return the members of specified key identified by the
``name`` argument, which are within the borders of the
area specified by a given shape. This command extends the
GEORADIUS command, so in addition to searching within circular
areas, it supports searching within rectangular areas.
This command should be used in place of the deprecated
GEORADIUS and GEORADIUSBYMEMBER commands.
``member`` Use the position of the given existing
member in the sorted set. Can't be given with ``longitude``
and ``latitude``.
``longitude`` and ``latitude`` Use the position given by
this coordinates. Can't be given with ``member``
``radius`` Similar to GEORADIUS, search inside circular
area according the given radius. Can't be given with
``height`` and ``width``.
``height`` and ``width`` Search inside an axis-aligned
rectangle, determined by the given height and width.
Can't be given with ``radius``
``unit`` must be one of the following : m, km, mi, ft.
`m` for meters (the default value), `km` for kilometers,
`mi` for miles and `ft` for feet.
``sort`` indicates to return the places in a sorted way,
ASC for nearest to farest and DESC for farest to nearest.
``count`` limit the results to the first count matching items.
``any`` is set to True, the command will return as soon as
enough matches are found. Can't be provided without ``count``
``withdist`` indicates to return the distances of each place.
``withcoord`` indicates to return the latitude and longitude of
each place.
``withhash`` indicates to return the geohash string of each place.
"""

return self._geosearchgeneric('GEOSEARCH',
name, member=member, longitude=longitude,
latitude=latitude, unit=unit,
radius=radius, width=width,
height=height, sort=sort, count=count,
any=any, withcoord=withcoord,
withdist=withdist, withhash=withhash,
store=None, store_dist=None)

def geosearchstore(self, dest, name, member=None, longitude=None,
latitude=None, unit='m', radius=None, width=None,
height=None, sort=None, count=None, any=False,
storedist=False):
"""
This command is like GEOSEARCH, but stores the result in
``dest``. By default, it stores the results in the destination
sorted set with their geospatial information.
if ``store_dist`` set to True, the command will stores the
items in a sorted set populated with their distance from the
center of the circle or box, as a floating-point number.
"""
return self._geosearchgeneric('GEOSEARCHSTORE',
dest, name, member=member,
longitude=longitude, latitude=latitude,
unit=unit, radius=radius, width=width,
height=height, sort=sort, count=count,
any=any, withcoord=None,
withdist=None, withhash=None,
store=None, store_dist=storedist)

def _geosearchgeneric(self, command, *args, **kwargs):
pieces = list(args)

# FROMMEMBER or FROMLONLAT
if kwargs['member'] is None:
if kwargs['longitude'] is None or kwargs['latitude'] is None:
raise DataError("GEOSEARCH must have member or"
" longitude and latitude")
if kwargs['member']:
if kwargs['longitude'] or kwargs['latitude']:
raise DataError("GEOSEARCH member and longitude or latitude"
" cant be set together")
pieces.extend([b'FROMMEMBER', kwargs['member']])
if kwargs['longitude'] and kwargs['latitude']:
pieces.extend([b'FROMLONLAT',
kwargs['longitude'], kwargs['latitude']])

# BYRADIUS or BYBOX
if kwargs['radius'] is None:
if kwargs['width'] is None or kwargs['height'] is None:
raise DataError("GEOSEARCH must have radius or"
" width and height")
if kwargs['unit'] is None:
raise DataError("GEOSEARCH must have unit")
if kwargs['unit'].lower() not in ('m', 'km', 'mi', 'ft'):
raise DataError("GEOSEARCH invalid unit")
if kwargs['radius']:
if kwargs['width'] or kwargs['height']:
raise DataError("GEOSEARCH radius and width or height"
" cant be set together")
pieces.extend([b'BYRADIUS', kwargs['radius'], kwargs['unit']])
if kwargs['width'] and kwargs['height']:
pieces.extend([b'BYBOX',
kwargs['width'], kwargs['height'], kwargs['unit']])

# sort
if kwargs['sort']:
if kwargs['sort'].upper() == 'ASC':
pieces.append(b'ASC')
elif kwargs['sort'].upper() == 'DESC':
pieces.append(b'DESC')
else:
raise DataError("GEOSEARCH invalid sort")

# count any
if kwargs['count']:
pieces.extend([b'COUNT', kwargs['count']])
if kwargs['any']:
pieces.append(b'ANY')
elif kwargs['any']:
raise DataError("GEOSEARCH any can't be provided without count")

# other properties
for arg_name, byte_repr in (
('withdist', b'WITHDIST'),
('withcoord', b'WITHCOORD'),
('withhash', b'WITHHASH'),
('store_dist', b'STOREDIST')):
if kwargs[arg_name]:
pieces.append(byte_repr)

return self.execute_command(command, *pieces, **kwargs)

# MODULE COMMANDS
def module_load(self, path):
"""
Expand Down
Loading