redis 4.1.0 → 4.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +158 -0
  3. data/README.md +91 -27
  4. data/lib/redis/client.rb +148 -92
  5. data/lib/redis/cluster/command.rb +4 -6
  6. data/lib/redis/cluster/command_loader.rb +6 -7
  7. data/lib/redis/cluster/node.rb +17 -1
  8. data/lib/redis/cluster/node_key.rb +3 -7
  9. data/lib/redis/cluster/option.rb +30 -14
  10. data/lib/redis/cluster/slot.rb +30 -13
  11. data/lib/redis/cluster/slot_loader.rb +4 -4
  12. data/lib/redis/cluster.rb +46 -17
  13. data/lib/redis/commands/bitmaps.rb +63 -0
  14. data/lib/redis/commands/cluster.rb +45 -0
  15. data/lib/redis/commands/connection.rb +58 -0
  16. data/lib/redis/commands/geo.rb +84 -0
  17. data/lib/redis/commands/hashes.rb +251 -0
  18. data/lib/redis/commands/hyper_log_log.rb +37 -0
  19. data/lib/redis/commands/keys.rb +411 -0
  20. data/lib/redis/commands/lists.rb +289 -0
  21. data/lib/redis/commands/pubsub.rb +72 -0
  22. data/lib/redis/commands/scripting.rb +114 -0
  23. data/lib/redis/commands/server.rb +188 -0
  24. data/lib/redis/commands/sets.rb +207 -0
  25. data/lib/redis/commands/sorted_sets.rb +804 -0
  26. data/lib/redis/commands/streams.rb +382 -0
  27. data/lib/redis/commands/strings.rb +313 -0
  28. data/lib/redis/commands/transactions.rb +92 -0
  29. data/lib/redis/commands.rb +242 -0
  30. data/lib/redis/connection/command_helper.rb +5 -2
  31. data/lib/redis/connection/hiredis.rb +7 -5
  32. data/lib/redis/connection/registry.rb +2 -1
  33. data/lib/redis/connection/ruby.rb +129 -110
  34. data/lib/redis/connection/synchrony.rb +17 -10
  35. data/lib/redis/connection.rb +3 -1
  36. data/lib/redis/distributed.rb +209 -70
  37. data/lib/redis/errors.rb +2 -0
  38. data/lib/redis/hash_ring.rb +15 -14
  39. data/lib/redis/pipeline.rb +139 -8
  40. data/lib/redis/subscribe.rb +11 -12
  41. data/lib/redis/version.rb +3 -1
  42. data/lib/redis.rb +167 -3377
  43. metadata +32 -25
@@ -15,36 +15,36 @@ class Redis
15
15
  def initialize(options)
16
16
  options = options.dup
17
17
  node_addrs = options.delete(:cluster)
18
- @node_uris = build_node_uris(node_addrs)
18
+ @node_opts = build_node_options(node_addrs)
19
19
  @replica = options.delete(:replica) == true
20
+ add_common_node_option_if_needed(options, @node_opts, :scheme)
21
+ add_common_node_option_if_needed(options, @node_opts, :username)
22
+ add_common_node_option_if_needed(options, @node_opts, :password)
20
23
  @options = options
21
24
  end
22
25
 
23
26
  def per_node_key
24
- @node_uris.map { |uri| [NodeKey.build_from_uri(uri), @options.merge(url: uri.to_s)] }
27
+ @node_opts.map { |opt| [NodeKey.build_from_host_port(opt[:host], opt[:port]), @options.merge(opt)] }
25
28
  .to_h
26
29
  end
27
30
 
28
- def secure?
29
- @node_uris.any? { |uri| uri.scheme == SECURE_SCHEME } || @options[:ssl_params] || false
30
- end
31
-
32
31
  def use_replica?
33
32
  @replica
34
33
  end
35
34
 
36
35
  def update_node(addrs)
37
- @node_uris = build_node_uris(addrs)
36
+ @node_opts = build_node_options(addrs)
38
37
  end
39
38
 
40
39
  def add_node(host, port)
41
- @node_uris << parse_node_hash(host: host, port: port)
40
+ @node_opts << { host: host, port: port }
42
41
  end
43
42
 
44
43
  private
45
44
 
46
- def build_node_uris(addrs)
45
+ def build_node_options(addrs)
47
46
  raise InvalidClientOptionError, 'Redis option of `cluster` must be an Array' unless addrs.is_a?(Array)
47
+
48
48
  addrs.map { |addr| parse_node_addr(addr) }
49
49
  end
50
50
 
@@ -53,7 +53,7 @@ class Redis
53
53
  when String
54
54
  parse_node_url(addr)
55
55
  when Hash
56
- parse_node_hash(addr)
56
+ parse_node_option(addr)
57
57
  else
58
58
  raise InvalidClientOptionError, 'Redis option of `cluster` must includes String or Hash'
59
59
  end
@@ -62,15 +62,31 @@ class Redis
62
62
  def parse_node_url(addr)
63
63
  uri = URI(addr)
64
64
  raise InvalidClientOptionError, "Invalid uri scheme #{addr}" unless VALID_SCHEMES.include?(uri.scheme)
65
- uri
65
+
66
+ db = uri.path.split('/')[1]&.to_i
67
+
68
+ { scheme: uri.scheme, username: uri.user, password: uri.password, host: uri.host, port: uri.port, db: db }
69
+ .reject { |_, v| v.nil? || v == '' }
66
70
  rescue URI::InvalidURIError => err
67
71
  raise InvalidClientOptionError, err.message
68
72
  end
69
73
 
70
- def parse_node_hash(addr)
74
+ def parse_node_option(addr)
71
75
  addr = addr.map { |k, v| [k.to_sym, v] }.to_h
72
- raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys' if addr.values_at(:host, :port).any?(&:nil?)
73
- URI::Generic.build(scheme: DEFAULT_SCHEME, host: addr[:host], port: addr[:port].to_i)
76
+ if addr.values_at(:host, :port).any?(&:nil?)
77
+ raise InvalidClientOptionError, 'Redis option of `cluster` must includes `:host` and `:port` keys'
78
+ end
79
+
80
+ addr
81
+ end
82
+
83
+ # Redis cluster node returns only host and port information.
84
+ # So we should complement additional information such as:
85
+ # scheme, username, password and so on.
86
+ def add_common_node_option_if_needed(options, node_opts, key)
87
+ return options if options[key].nil? && node_opts.first[key].nil?
88
+
89
+ options[key] ||= node_opts.first[key]
74
90
  end
75
91
  end
76
92
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'set'
4
-
5
3
  class Redis
6
4
  class Cluster
7
5
  # Keep slot and node key map for Redis Cluster Client
@@ -28,11 +26,20 @@ class Redis
28
26
  return nil unless exists?(slot)
29
27
  return find_node_key_of_master(slot) if replica_disabled?
30
28
 
31
- @map[slot][:slaves].to_a.sample
29
+ @map[slot][:slaves].sample
32
30
  end
33
31
 
34
32
  def put(slot, node_key)
35
- assign_node_key(@map, slot, node_key)
33
+ # Since we're sharing a hash for build_slot_node_key_map, duplicate it
34
+ # if it already exists instead of preserving as-is.
35
+ @map[slot] = @map[slot] ? @map[slot].dup : { master: nil, slaves: [] }
36
+
37
+ if master?(node_key)
38
+ @map[slot][:master] = node_key
39
+ elsif !@map[slot][:slaves].include?(node_key)
40
+ @map[slot][:slaves] << node_key
41
+ end
42
+
36
43
  nil
37
44
  end
38
45
 
@@ -50,19 +57,29 @@ class Redis
50
57
  @node_flags[node_key] == ROLE_SLAVE
51
58
  end
52
59
 
60
+ # available_slots is mapping of node_key to list of slot ranges
53
61
  def build_slot_node_key_map(available_slots)
54
- available_slots.each_with_object({}) do |(node_key, slots), acc|
55
- slots.each { |slot| assign_node_key(acc, slot, node_key) }
62
+ by_ranges = {}
63
+ available_slots.each do |node_key, slots_arr|
64
+ by_ranges[slots_arr] ||= { master: nil, slaves: [] }
65
+
66
+ if master?(node_key)
67
+ by_ranges[slots_arr][:master] = node_key
68
+ elsif !by_ranges[slots_arr][:slaves].include?(node_key)
69
+ by_ranges[slots_arr][:slaves] << node_key
70
+ end
56
71
  end
57
- end
58
72
 
59
- def assign_node_key(mappings, slot, node_key)
60
- mappings[slot] ||= { master: nil, slaves: Set.new }
61
- if master?(node_key)
62
- mappings[slot][:master] = node_key
63
- else
64
- mappings[slot][:slaves].add(node_key)
73
+ by_slot = {}
74
+ by_ranges.each do |slots_arr, nodes|
75
+ slots_arr.each do |slots|
76
+ slots.each do |slot|
77
+ by_slot[slot] = nodes
78
+ end
79
+ end
65
80
  end
81
+
82
+ by_slot
66
83
  end
67
84
  end
68
85
  end
@@ -13,7 +13,7 @@ class Redis
13
13
  info = {}
14
14
 
15
15
  nodes.each do |node|
16
- info = Hash[*fetch_slot_info(node)]
16
+ info = fetch_slot_info(node)
17
17
  info.empty? ? next : break
18
18
  end
19
19
 
@@ -23,9 +23,10 @@ class Redis
23
23
  end
24
24
 
25
25
  def fetch_slot_info(node)
26
+ hash_with_default_arr = Hash.new { |h, k| h[k] = [] }
26
27
  node.call(%i[cluster slots])
27
- .map { |arr| parse_slot_info(arr, default_ip: node.host) }
28
- .flatten
28
+ .flat_map { |arr| parse_slot_info(arr, default_ip: node.host) }
29
+ .each_with_object(hash_with_default_arr) { |arr, h| h[arr[0]] << arr[1] }
29
30
  rescue CannotConnectError, ConnectionError, CommandError
30
31
  {} # can retry on another node
31
32
  end
@@ -34,7 +35,6 @@ class Redis
34
35
  first_slot, last_slot = arr[0..1]
35
36
  slot_range = (first_slot..last_slot).freeze
36
37
  arr[2..-1].map { |addr| [stringify_node_key(addr, default_ip), slot_range] }
37
- .flatten
38
38
  end
39
39
 
40
40
  def stringify_node_key(arr, default_ip)
data/lib/redis/cluster.rb CHANGED
@@ -78,10 +78,13 @@ class Redis
78
78
  end
79
79
 
80
80
  def call_pipeline(pipeline)
81
- node_keys, command_keys = extract_keys_in_pipeline(pipeline)
82
- raise CrossSlotPipeliningError, command_keys if node_keys.size > 1
83
- node = find_node(node_keys.first)
84
- try_send(node, :call_pipeline, pipeline)
81
+ node_keys = pipeline.commands.map { |cmd| find_node_key(cmd, primary_only: true) }.compact.uniq
82
+ if node_keys.size > 1
83
+ raise(CrossSlotPipeliningError,
84
+ pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?).uniq)
85
+ end
86
+
87
+ try_send(find_node(node_keys.first), :call_pipeline, pipeline)
85
88
  end
86
89
 
87
90
  def call_with_timeout(command, timeout, &block)
@@ -112,12 +115,11 @@ class Redis
112
115
  node = Node.new(option.per_node_key)
113
116
  available_slots = SlotLoader.load(node)
114
117
  node_flags = NodeLoader.load_flags(node)
115
- available_node_urls = NodeKey.to_node_urls(available_slots.keys, secure: option.secure?)
116
- option.update_node(available_node_urls)
118
+ option.update_node(available_slots.keys.map { |k| NodeKey.optionize(k) })
117
119
  [Node.new(option.per_node_key, node_flags, option.use_replica?),
118
120
  Slot.new(available_slots, node_flags, option.use_replica?)]
119
121
  ensure
120
- node.map(&:disconnect)
122
+ node&.each(&:disconnect)
121
123
  end
122
124
 
123
125
  def fetch_command_details(nodes)
@@ -128,13 +130,14 @@ class Redis
128
130
  def send_command(command, &block)
129
131
  cmd = command.first.to_s.downcase
130
132
  case cmd
131
- when 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
133
+ when 'acl', 'auth', 'bgrewriteaof', 'bgsave', 'quit', 'save'
132
134
  @node.call_all(command, &block).first
133
135
  when 'flushall', 'flushdb'
134
136
  @node.call_master(command, &block).first
135
137
  when 'wait' then @node.call_master(command, &block).reduce(:+)
136
138
  when 'keys' then @node.call_slave(command, &block).flatten.sort
137
139
  when 'dbsize' then @node.call_slave(command, &block).reduce(:+)
140
+ when 'scan' then _scan(command, &block)
138
141
  when 'lastsave' then @node.call_all(command, &block).sort
139
142
  when 'role' then @node.call_all(command, &block)
140
143
  when 'config' then send_config_command(command, &block)
@@ -216,9 +219,14 @@ class Redis
216
219
  node.public_send(method_name, *args, &block)
217
220
  rescue CommandError => err
218
221
  if err.message.start_with?('MOVED')
219
- assign_redirection_node(err.message).public_send(method_name, *args, &block)
222
+ raise if retry_count <= 0
223
+
224
+ node = assign_redirection_node(err.message)
225
+ retry_count -= 1
226
+ retry
220
227
  elsif err.message.start_with?('ASK')
221
228
  raise if retry_count <= 0
229
+
222
230
  node = assign_asking_node(err.message)
223
231
  node.call(%i[asking])
224
232
  retry_count -= 1
@@ -226,6 +234,32 @@ class Redis
226
234
  else
227
235
  raise
228
236
  end
237
+ rescue CannotConnectError
238
+ update_cluster_info!
239
+ raise
240
+ end
241
+
242
+ def _scan(command, &block)
243
+ input_cursor = Integer(command[1])
244
+
245
+ client_index = input_cursor % 256
246
+ raw_cursor = input_cursor >> 8
247
+
248
+ clients = @node.scale_reading_clients
249
+
250
+ client = clients[client_index]
251
+ return ['0', []] unless client
252
+
253
+ command[1] = raw_cursor.to_s
254
+
255
+ result_cursor, result_keys = client.call(command, &block)
256
+ result_cursor = Integer(result_cursor)
257
+
258
+ if result_cursor == 0
259
+ client_index += 1
260
+ end
261
+
262
+ [((result_cursor << 8) + client_index).to_s, result_keys]
229
263
  end
230
264
 
231
265
  def assign_redirection_node(err_msg)
@@ -245,14 +279,14 @@ class Redis
245
279
  find_node(node_key)
246
280
  end
247
281
 
248
- def find_node_key(command)
282
+ def find_node_key(command, primary_only: false)
249
283
  key = @command.extract_first_key(command)
250
284
  return if key.empty?
251
285
 
252
286
  slot = KeySlotConverter.convert(key)
253
287
  return unless @slot.exists?(slot)
254
288
 
255
- if @command.should_send_to_master?(command)
289
+ if @command.should_send_to_master?(command) || primary_only
256
290
  @slot.find_node_key_of_master(slot)
257
291
  else
258
292
  @slot.find_node_key_of_slave(slot)
@@ -261,6 +295,7 @@ class Redis
261
295
 
262
296
  def find_node(node_key)
263
297
  return @node.sample if node_key.nil?
298
+
264
299
  @node.find_by(node_key)
265
300
  rescue Node::ReloadNeeded
266
301
  update_cluster_info!(node_key)
@@ -276,11 +311,5 @@ class Redis
276
311
  @node.map(&:disconnect)
277
312
  @node, @slot = fetch_cluster_info!(@option)
278
313
  end
279
-
280
- def extract_keys_in_pipeline(pipeline)
281
- node_keys = pipeline.commands.map { |cmd| find_node_key(cmd) }.compact.uniq
282
- command_keys = pipeline.commands.map { |cmd| @command.extract_first_key(cmd) }.reject(&:empty?)
283
- [node_keys, command_keys]
284
- end
285
314
  end
286
315
  end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Commands
5
+ module Bitmaps
6
+ # Sets or clears the bit at offset in the string value stored at key.
7
+ #
8
+ # @param [String] key
9
+ # @param [Integer] offset bit offset
10
+ # @param [Integer] value bit value `0` or `1`
11
+ # @return [Integer] the original bit value stored at `offset`
12
+ def setbit(key, offset, value)
13
+ send_command([:setbit, key, offset, value])
14
+ end
15
+
16
+ # Returns the bit value at offset in the string value stored at key.
17
+ #
18
+ # @param [String] key
19
+ # @param [Integer] offset bit offset
20
+ # @return [Integer] `0` or `1`
21
+ def getbit(key, offset)
22
+ send_command([:getbit, key, offset])
23
+ end
24
+
25
+ # Count the number of set bits in a range of the string value stored at key.
26
+ #
27
+ # @param [String] key
28
+ # @param [Integer] start start index
29
+ # @param [Integer] stop stop index
30
+ # @return [Integer] the number of bits set to 1
31
+ def bitcount(key, start = 0, stop = -1)
32
+ send_command([:bitcount, key, start, stop])
33
+ end
34
+
35
+ # Perform a bitwise operation between strings and store the resulting string in a key.
36
+ #
37
+ # @param [String] operation e.g. `and`, `or`, `xor`, `not`
38
+ # @param [String] destkey destination key
39
+ # @param [String, Array<String>] keys one or more source keys to perform `operation`
40
+ # @return [Integer] the length of the string stored in `destkey`
41
+ def bitop(operation, destkey, *keys)
42
+ send_command([:bitop, operation, destkey, *keys])
43
+ end
44
+
45
+ # Return the position of the first bit set to 1 or 0 in a string.
46
+ #
47
+ # @param [String] key
48
+ # @param [Integer] bit whether to look for the first 1 or 0 bit
49
+ # @param [Integer] start start index
50
+ # @param [Integer] stop stop index
51
+ # @return [Integer] the position of the first 1/0 bit.
52
+ # -1 if looking for 1 and it is not found or start and stop are given.
53
+ def bitpos(key, bit, start = nil, stop = nil)
54
+ raise(ArgumentError, 'stop parameter specified without start parameter') if stop && !start
55
+
56
+ command = [:bitpos, key, bit]
57
+ command << start if start
58
+ command << stop if stop
59
+ send_command(command)
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Commands
5
+ module Cluster
6
+ # Sends `CLUSTER *` command to random node and returns its reply.
7
+ #
8
+ # @see https://redis.io/commands#cluster Reference of cluster command
9
+ #
10
+ # @param subcommand [String, Symbol] the subcommand of cluster command
11
+ # e.g. `:slots`, `:nodes`, `:slaves`, `:info`
12
+ #
13
+ # @return [Object] depends on the subcommand
14
+ def cluster(subcommand, *args)
15
+ subcommand = subcommand.to_s.downcase
16
+ block = case subcommand
17
+ when 'slots'
18
+ HashifyClusterSlots
19
+ when 'nodes'
20
+ HashifyClusterNodes
21
+ when 'slaves'
22
+ HashifyClusterSlaves
23
+ when 'info'
24
+ HashifyInfo
25
+ else
26
+ Noop
27
+ end
28
+
29
+ # @see https://github.com/antirez/redis/blob/unstable/src/redis-trib.rb#L127 raw reply expected
30
+ block = Noop unless @cluster_mode
31
+
32
+ send_command([:cluster, subcommand] + args, &block)
33
+ end
34
+
35
+ # Sends `ASKING` command to random node and returns its reply.
36
+ #
37
+ # @see https://redis.io/topics/cluster-spec#ask-redirection ASK redirection
38
+ #
39
+ # @return [String] `'OK'`
40
+ def asking
41
+ send_command(%i[asking])
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Commands
5
+ module Connection
6
+ # Authenticate to the server.
7
+ #
8
+ # @param [Array<String>] args includes both username and password
9
+ # or only password
10
+ # @return [String] `OK`
11
+ # @see https://redis.io/commands/auth AUTH command
12
+ def auth(*args)
13
+ send_command([:auth, *args])
14
+ end
15
+
16
+ # Ping the server.
17
+ #
18
+ # @param [optional, String] message
19
+ # @return [String] `PONG`
20
+ def ping(message = nil)
21
+ send_command([:ping, message].compact)
22
+ end
23
+
24
+ # Echo the given string.
25
+ #
26
+ # @param [String] value
27
+ # @return [String]
28
+ def echo(value)
29
+ send_command([:echo, value])
30
+ end
31
+
32
+ # Change the selected database for the current connection.
33
+ #
34
+ # @param [Integer] db zero-based index of the DB to use (0 to 15)
35
+ # @return [String] `OK`
36
+ def select(db)
37
+ synchronize do |client|
38
+ client.db = db
39
+ client.call([:select, db])
40
+ end
41
+ end
42
+
43
+ # Close the connection.
44
+ #
45
+ # @return [String] `OK`
46
+ def quit
47
+ synchronize do |client|
48
+ begin
49
+ client.call([:quit])
50
+ rescue ConnectionError
51
+ ensure
52
+ client.disconnect
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Redis
4
+ module Commands
5
+ module Geo
6
+ # Adds the specified geospatial items (latitude, longitude, name) to the specified key
7
+ #
8
+ # @param [String] key
9
+ # @param [Array] member arguemnts for member or members: longitude, latitude, name
10
+ # @return [Integer] number of elements added to the sorted set
11
+ def geoadd(key, *member)
12
+ send_command([:geoadd, key, *member])
13
+ end
14
+
15
+ # Returns geohash string representing position for specified members of the specified key.
16
+ #
17
+ # @param [String] key
18
+ # @param [String, Array<String>] member one member or array of members
19
+ # @return [Array<String, nil>] returns array containg geohash string if member is present, nil otherwise
20
+ def geohash(key, member)
21
+ send_command([:geohash, key, member])
22
+ end
23
+
24
+ # Query a sorted set representing a geospatial index to fetch members matching a
25
+ # given maximum distance from a point
26
+ #
27
+ # @param [Array] args key, longitude, latitude, radius, unit(m|km|ft|mi)
28
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest
29
+ # or the farthest to the nearest relative to the center
30
+ # @param [Integer] count limit the results to the first N matching items
31
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
32
+ # @return [Array<String>] may be changed with `options`
33
+ def georadius(*args, **geoptions)
34
+ geoarguments = _geoarguments(*args, **geoptions)
35
+
36
+ send_command([:georadius, *geoarguments])
37
+ end
38
+
39
+ # Query a sorted set representing a geospatial index to fetch members matching a
40
+ # given maximum distance from an already existing member
41
+ #
42
+ # @param [Array] args key, member, radius, unit(m|km|ft|mi)
43
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest
44
+ # to the nearest relative to the center
45
+ # @param [Integer] count limit the results to the first N matching items
46
+ # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
47
+ # @return [Array<String>] may be changed with `options`
48
+ def georadiusbymember(*args, **geoptions)
49
+ geoarguments = _geoarguments(*args, **geoptions)
50
+
51
+ send_command([:georadiusbymember, *geoarguments])
52
+ end
53
+
54
+ # Returns longitude and latitude of members of a geospatial index
55
+ #
56
+ # @param [String] key
57
+ # @param [String, Array<String>] member one member or array of members
58
+ # @return [Array<Array<String>, nil>] returns array of elements, where each
59
+ # element is either array of longitude and latitude or nil
60
+ def geopos(key, member)
61
+ send_command([:geopos, key, member])
62
+ end
63
+
64
+ # Returns the distance between two members of a geospatial index
65
+ #
66
+ # @param [String ]key
67
+ # @param [Array<String>] members
68
+ # @param ['m', 'km', 'mi', 'ft'] unit
69
+ # @return [String, nil] returns distance in spefied unit if both members present, nil otherwise.
70
+ def geodist(key, member1, member2, unit = 'm')
71
+ send_command([:geodist, key, member1, member2, unit])
72
+ end
73
+
74
+ private
75
+
76
+ def _geoarguments(*args, options: nil, sort: nil, count: nil)
77
+ args.push sort if sort
78
+ args.push 'count', count if count
79
+ args.push options if options
80
+ args
81
+ end
82
+ end
83
+ end
84
+ end