redis 5.4.1 → 6.0.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d76a0d8a8a0361f991dc110219ffc77c12503148f9ece9586dd274321283265
4
- data.tar.gz: fab1b1d4d3a5e22d2d952c98648a1412239f117a2c53bcbdf20ea7c43f456faa
3
+ metadata.gz: 10e2b37efa4ce556e0a412006412a027693dc20e5f45df8759d2571dec5008ab
4
+ data.tar.gz: 074126dd5ed435ac137b0b946129db11ef16020afd8a65dcbe94b181beaf9198
5
5
  SHA512:
6
- metadata.gz: 042f0aa785647bb0e6f73ff50722ba5dcf440e3f5e817c7eec030b876b7d34ca8be42a7a5a5a1767eba66b7424a8d2e74fe8e7b9e0362eedefe4fb58878cdb57
7
- data.tar.gz: 5bc22d99f3a77929f35c02072be18d663ca127c77055afadd55455d3f571f61bbd70853d6fd4414e0e11f601c20d68872e8f097fce109341e5bda39f6aadb9fe
6
+ metadata.gz: d5cc54f6d9f95a0d77c2f824569dc9d09506eabf593c9b9d18483bae897258daf1a1fa8e069b79388ac0f8948d288cfe4af3fcea9d3760a3074bc0c4b55cb2fb
7
+ data.tar.gz: a8137ff6826639d927179a78d0017604aa34f592bd9710a777a58db0618a8061eb71aa4ee402fae928b26dc6e43a19c4248181604db17b1a584760a806fedadf
data/CHANGELOG.md CHANGED
@@ -1,4 +1,55 @@
1
- # Unreleased
1
+ # 6.0.0
2
+
3
+ ## Breaking changes
4
+
5
+ - Use RESP3 (`HELLO 3`) by default; pass `protocol: 2` to keep RESP2. `GEOPOS` and
6
+ `GEOSEARCH`/`GEORADIUS` with `WITHCOORD` now return coordinates as `Float` instead of `String`.
7
+ See [specs/migration-resp3.md](specs/migration-resp3.md). (#1351)
8
+ - Require Ruby 3.2+. (#1353, #1365)
9
+
10
+ ## New features
11
+
12
+ - Add support for the Redis Query Engine (RediSearch, `FT.*`): `ft_create`, `ft_alter`,
13
+ `ft_dropindex`, `ft_info`, `ft_search`, `ft_aggregate`, vector and hybrid search, suggestions,
14
+ dictionaries, synonyms, aliases, and spellcheck. Not supported by `Redis::Distributed`.
15
+ (#1356, #1359, #1360, #1361)
16
+ - Add support for the JSON module: `json_set`, `json_get`, `json_mset`, `json_mget`, `json_del`,
17
+ `json_forget`, `json_clear`, `json_merge`, `json_arr*`, `json_obj*`, `json_str*`,
18
+ `json_numincrby`, `json_toggle`, `json_type`, `json_debug_memory`. (#1346, #1347, #1348, #1349)
19
+ - Add `lmovem` and `blmovem` (Redis 8.10). (#1363)
20
+ - Add `sunioncard` and `sdiffcard` (Redis 8.10). (#1357)
21
+ - `xread` and `xreadgroup` now accept `max_count:` and `max_size:` (Redis 8.10). (#1358)
22
+ - Add `hexpire`, `hpexpire`, `httl` and `hpttl` (Redis 7.4). (#1324, #1325, #1331)
23
+ - `hscan` and `hscan_each` now accept `novalues: true` (Redis 7.4). (#1327)
24
+ - Add `geosearch` and `geosearchstore` (Redis 6.2); geo commands are now available on
25
+ `Redis::Distributed`. (#1342)
26
+ - `geoadd` now accepts `nx:`, `xx:` and `ch:`; geo radius searches accept `count_any:`; `xadd`
27
+ accepts `limit:` (Redis 6.2). (#1345)
28
+ - `Redis::Distributed` now implements `hscan`, `hscan_each` and `hstrlen`. (#1319)
29
+ - Identify the client to the server via `CLIENT SETINFO` (`lib-name=redis-rb`,
30
+ `lib-ver=<version>`). Extend the reported name with `driver_info:`, or disable with
31
+ `driver_info: false`. See the README "Client identification" section. (#1369)
32
+
33
+ ## Experimental
34
+
35
+ - Add `himport_prepare`, `himport_set`, `himport_discard` and `himport_discard_all`
36
+ (Redis 8.10 `HIMPORT`). Lost fieldsets are re-prepared and retried automatically; disable with
37
+ `himport_auto_prepare: false`. The API may change in a future minor release. See the README
38
+ "Bulk hash ingestion (HIMPORT)" section. (#1364)
39
+
40
+ ## Bug fixes
41
+
42
+ - Fix `FloatifyPairs` to not re-transform already transformed replies. (#1354)
43
+ - `unlink` now returns `0` for an empty keyset, consistent with `del`. (#1316)
44
+
45
+ ## Maintenance
46
+
47
+ - Pin `redis-client` to the exact version `0.30.1` (previously `>= 0.22.0`); includes the
48
+ reply-desynchronization fix from 0.26.4. (#1350, #1352)
49
+ - Maintainership change: `redis-rb` is now maintained by the Redis Ltd company.
50
+ - Run the test suite against prebuilt `redislabs/client-libs-test` Docker images; CI now covers
51
+ MRI 3.2/3.3/3.4/4.0 and TruffleRuby against Redis 7.2–8.10; JRuby removed. (#1317, #1318, #1344)
52
+ - Drop the unused `executables` config from the gemspec. (#1322)
2
53
 
3
54
  # 5.4.1
4
55
 
data/README.md CHANGED
@@ -71,6 +71,97 @@ redis.get("mykey")
71
71
  All commands, their arguments, and return values are documented and
72
72
  available on [RubyDoc.info][rubydoc].
73
73
 
74
+ ## Language and server support
75
+
76
+ redis-rb targets actively supported runtimes on both the language and the server side:
77
+
78
+ - **Ruby:** Ruby 3.2 and newer. See the [Ruby maintenance branches][ruby-branches] page for
79
+ each version's status and dates.
80
+ - **Redis server:** the versions designated for support by Redis. See
81
+ [Supported Redis database versions][redis-versions].
82
+
83
+ ## Protocol (RESP3)
84
+
85
+ Starting in 6.0, the client negotiates the [RESP3 protocol][resp3] (`HELLO 3`)
86
+ by default. Command return values are unchanged from 5.x, with one exception:
87
+ `GEOPOS` and `GEOSEARCH`/`GEORADIUS` with `WITHCOORD` now return coordinates as
88
+ `Float` instead of `String`.
89
+
90
+ To keep the previous RESP2 behavior, pass `protocol: 2`:
91
+
92
+ ```ruby
93
+ redis = Redis.new(protocol: 2)
94
+ ```
95
+
96
+ Servers without RESP3 support (Redis < 6.0, or anything replying `NOPROTO`) are
97
+ detected on connect and the client transparently falls back to RESP2, so no
98
+ configuration is needed for older servers.
99
+
100
+ ### Why RESP3 is the default
101
+
102
+ RESP3's richer wire types let the parser deliver replies already in their final
103
+ Ruby shape. Under RESP2, structured replies arrive as flat arrays of bulk
104
+ strings and the client re-shapes them in Ruby: `HGETALL` turns a flat
105
+ `[field, value, field, value, ...]` array into a `Hash`, and sorted-set scores
106
+ are converted from `String` to `Float` pair by pair. Under RESP3 the server
107
+ tags these replies as native maps and doubles, so the final `Hash` and `Float`
108
+ values come straight out of the parser and the Ruby-side re-shaping pass
109
+ disappears entirely.
110
+
111
+ How much that saves depends on where parsing happens. In our benchmarks
112
+ ([bench/resp_comparison.rb](bench/resp_comparison.rb), Ruby 3.4, 100-element
113
+ replies), hash reads (`HGETALL`) consistently use ~10–25% less client CPU per
114
+ call on both drivers. With the [hiredis driver](#hiredis-binding), where
115
+ parsing runs in C, sorted-set reads with scores gain up to 16% throughput and
116
+ ~20% less CPU per call on top of that; with the pure-Ruby driver they are
117
+ unchanged, since the parser then spends in Ruby roughly what the re-shaping
118
+ pass used to cost. Simple string commands and stream commands are unaffected
119
+ either way — their reply shapes are the same in both protocols. In short:
120
+ RESP3 is never slower where it matters, and it pairs best with hiredis — that
121
+ combination moves all reply construction out of Ruby and into C.
122
+
123
+ Beyond performance, RESP3 unlocks protocol capabilities RESP2 simply doesn't
124
+ have. The most important is out-of-band **push messages**: the server can send
125
+ notifications on a connection without the client asking, which is the
126
+ foundation for server-assisted client-side caching (`CLIENT TRACKING`
127
+ invalidation events), pub/sub messages delivered over the regular command
128
+ connection instead of a dedicated one, and other server-initiated
129
+ notifications. Defaulting to RESP3 in 6.0 lays the groundwork for building
130
+ these features in future releases without another protocol migration.
131
+
132
+ See [the RESP3 migration guide](specs/migration-resp3.md) for full details.
133
+
134
+ ## Client identification
135
+
136
+ On connect the client identifies itself to the server with `CLIENT SETINFO`, so
137
+ `redis-rb` and its version are visible in `CLIENT LIST` and `CLIENT INFO`:
138
+
139
+ ```
140
+ lib-name=redis-rb lib-ver=<Redis::VERSION>
141
+ ```
142
+
143
+ Libraries built on top of `redis-rb` can add their own identity with
144
+ `driver_info:`, which is reported alongside it. The recommended suffix format
145
+ is `<name>_v<version>`, following the convention used by the official client
146
+ libraries:
147
+
148
+ ```ruby
149
+ Redis.new(driver_info: "my-gem_v#{MyGem::VERSION}")
150
+ # reported as: lib-name=redis-rb(my-gem_v1.0.0) lib-ver=<Redis::VERSION>
151
+ ```
152
+
153
+ `driver_info:` also accepts an array, joined with `;` (the conventional
154
+ delimiter for multiple suffixes). It extends the reported name rather than
155
+ replacing it, so `redis-rb` stays identifiable either way. Runs of characters
156
+ the server would reject (spaces, non-printable bytes) and of the parentheses
157
+ that delimit the suffix are each replaced with a single `_`, or dropped at the
158
+ edges of the value.
159
+
160
+ Servers older than 7.2 don't support `CLIENT SETINFO`; they reject it, the error
161
+ is ignored, and the connection is used as normal. If a proxy or server can't
162
+ tolerate the command at all, pass `driver_info: false` to disable client
163
+ identification entirely.
164
+
74
165
  ## Connection Pooling and Thread safety
75
166
 
76
167
  The client does not provide connection pooling. Each `Redis` instance
@@ -94,7 +185,7 @@ MyApp.redis.incr("some-counter")
94
185
  ## Sentinel support
95
186
 
96
187
  The client is able to perform automatic failover by using [Redis
97
- Sentinel](http://redis.io/topics/sentinel). Make sure to run Redis 2.8+
188
+ Sentinel](http://redis.io/topics/sentinel). Make sure to run Redis 2.8+
98
189
  if you want to use this feature.
99
190
 
100
191
  To connect using Sentinel, use:
@@ -114,13 +205,13 @@ and `slave`. When the role is `slave`, the client will try to connect to a
114
205
  random slave of the specified master. If a role is not specified, the client
115
206
  will connect to the master.
116
207
 
117
- * When using the Sentinel support you need to specify a list of sentinels to
208
+ * When using Sentinel support, you need to specify a list of sentinels to
118
209
  connect to. The list does not need to enumerate all your Sentinel instances,
119
210
  but a few so that if one is down the client will try the next one. The client
120
211
  is able to remember the last Sentinel that was able to reply correctly and will
121
- use it for the next requests.
212
+ use it for the next request.
122
213
 
123
- To [authenticate](https://redis.io/docs/management/sentinel/#configuring-sentinel-instances-with-authentication) Sentinel itself, you can specify the `sentinel_username` and `sentinel_password`. Exclude the `sentinel_username` option if you're using password-only authentication.
214
+ To [authenticate](https://redis.io/docs/management/sentinel/#configuring-sentinel-instances-with-authentication) with Sentinel itself, you can specify the `sentinel_username` and `sentinel_password`. Exclude the `sentinel_username` option if you're using password-only authentication.
124
215
 
125
216
  ```ruby
126
217
  SENTINELS = [{ host: '127.0.0.1', port: 26380},
@@ -129,7 +220,7 @@ SENTINELS = [{ host: '127.0.0.1', port: 26380},
129
220
  redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, sentinel_username: 'appuser', sentinel_password: 'mysecret', role: :master)
130
221
  ```
131
222
 
132
- If you specify a username and/or password at the top level for your main Redis instance, Sentinel *will not* using thouse credentials
223
+ If you specify a username and/or password at the top level for your main Redis instance, Sentinel *will not* use those credentials.
133
224
 
134
225
  ```ruby
135
226
  # Use 'mysecret' to authenticate against the mymaster instance, but skip authentication for the sentinels:
@@ -139,7 +230,7 @@ SENTINELS = [{ host: '127.0.0.1', port: 26380 },
139
230
  redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret')
140
231
  ```
141
232
 
142
- So you have to provide Sentinel credential and Redis explicitly even they are the same
233
+ So you have to provide Sentinel credentials and Redis explicitly even if they are the same.
143
234
 
144
235
  ```ruby
145
236
  # Use 'mysecret' to authenticate against the mymaster instance and sentinel
@@ -149,7 +240,7 @@ SENTINELS = [{ host: '127.0.0.1', port: 26380 },
149
240
  redis = Redis.new(name: 'mymaster', sentinels: SENTINELS, role: :master, password: 'mysecret', sentinel_password: 'mysecret')
150
241
  ```
151
242
 
152
- Also the `name`, `password`, `username` and `db` for Redis instance can be passed as an url:
243
+ Also, the `name`, `password`, `username`, and `db` for the Redis instance can be passed as a URL:
153
244
 
154
245
  ```ruby
155
246
  redis = Redis.new(url: "redis://appuser:mysecret@mymaster/10", sentinels: SENTINELS, role: :master)
@@ -251,6 +342,70 @@ incr.value
251
342
  # => 1
252
343
  ```
253
344
 
345
+ ## Bulk hash ingestion (HIMPORT)
346
+
347
+ > **Experimental:** HIMPORT support is experimental. The client API (method
348
+ > signatures, reply aggregation on cluster, and the automatic re-prepare
349
+ > behavior) may change in a future minor release without a major version bump.
350
+
351
+ Redis 8.10 adds the `HIMPORT` command family for loading many hashes that share
352
+ the same set of field names: register the field names once with
353
+ `himport_prepare`, then create each hash by sending only its values. Keys
354
+ written this way are regular hashes — every hash command works on them.
355
+
356
+ ```ruby
357
+ redis.himport_prepare("users", ["name", "email", "age"])
358
+ redis.himport_set("user:1", "users", ["alice", "alice@example.com", "25"])
359
+ redis.himport_set("user:2", "users", ["bob", "bob@example.com", "30"])
360
+ redis.himport_discard("users") # => 1
361
+ ```
362
+
363
+ Values pair positionally with the prepared fields. Note that hash enumeration
364
+ order (`HGETALL`, `HKEYS`) is not guaranteed to match the prepare order.
365
+
366
+ ### Fieldsets are connection state
367
+
368
+ A prepared fieldset lives in the server-side session of the physical connection
369
+ that prepared it: it is invisible to other connections and destroyed by a
370
+ disconnect or `RESET`. A `himport_set` on a connection without the fieldset
371
+ fails with `ERR no such fieldset`.
372
+
373
+ Because a `Redis` instance transparently replaces a dead connection (see
374
+ [Reconnections](#reconnections)), the client keeps the last schema prepared for
375
+ each fieldset name and, when a `himport_set` reports the fieldset is gone,
376
+ re-prepares it and retries the command once. Explicitly discarded fieldsets are
377
+ never restored. To keep the fieldset lifecycle fully explicit instead, disable
378
+ the recovery:
379
+
380
+ ```ruby
381
+ redis = Redis.new(himport_auto_prepare: false)
382
+ ```
383
+
384
+ For the highest ingestion throughput, send the `PREPARE` and its `SET`s as one
385
+ pipeline — a single batch always executes on a single connection:
386
+
387
+ ```ruby
388
+ redis.pipelined do |pipeline|
389
+ pipeline.himport_prepare("users", ["name", "email", "age"])
390
+ rows.each { |id, row| pipeline.himport_set("user:#{id}", "users", row) }
391
+ end
392
+ ```
393
+
394
+ If the `PREPARE` in a batch fails, every `SET` in it fails with
395
+ `no such fieldset` — the `PREPARE` error is the root cause. Note that the
396
+ automatic re-prepare applies to direct calls only, not to commands inside
397
+ `pipelined`/`multi` blocks.
398
+
399
+ When using the `connection_pool` gem, each checkout may hand you a different
400
+ underlying connection: run `himport_prepare` and its `himport_set` calls within
401
+ one checkout (`pool.with { |redis| ... }`), ideally as one pipelined block.
402
+
403
+ With `Redis::Distributed`, `himport_prepare`, `himport_discard` and
404
+ `himport_discard_all` fan out to every ring node and return an array with one
405
+ reply per node; `himport_set` routes by key. With `Redis::Cluster`, the same
406
+ commands fan out to every master node and return a single aggregated reply,
407
+ matching the standalone API.
408
+
254
409
  ## Error Handling
255
410
 
256
411
  In general, if something goes wrong you'll get an exception. For example, if
@@ -421,11 +576,6 @@ the client object, specify hiredis:
421
576
  redis = Redis.new(driver: :hiredis)
422
577
  ```
423
578
 
424
- ## Testing
425
-
426
- This library is tested against recent Ruby and Redis versions.
427
- Check [Github Actions][gh-actions-link] for the exact versions supported.
428
-
429
579
  ## See Also
430
580
 
431
581
  - [async-redis](https://github.com/socketry/async-redis) — An [async](https://github.com/socketry/async) compatible Redis client.
@@ -451,3 +601,6 @@ requests.
451
601
  [gh-actions-image]: https://github.com/redis/redis-rb/workflows/Test/badge.svg
452
602
  [gh-actions-link]: https://github.com/redis/redis-rb/actions
453
603
  [rubydoc]: https://rubydoc.info/gems/redis
604
+ [resp3]: https://github.com/redis/redis-specifications/blob/master/protocol/RESP3.md
605
+ [ruby-branches]: https://www.ruby-lang.org/en/downloads/branches/
606
+ [redis-versions]: https://redis.io/docs/latest/operate/rc/databases/version-management/#supported-database-versions
data/lib/redis/client.rb CHANGED
@@ -20,12 +20,15 @@ class Redis
20
20
  end
21
21
 
22
22
  class << self
23
- def config(**kwargs)
24
- super(protocol: 2, **kwargs)
23
+ def config(protocol: 3, **kwargs)
24
+ super(protocol: protocol, **kwargs,
25
+ driver_info: Redis::LibIdentity.driver_info(kwargs[:driver_info]))
25
26
  end
26
27
 
27
- def sentinel(**kwargs)
28
- super(protocol: 2, **kwargs, client_implementation: ::RedisClient)
28
+ def sentinel(protocol: 3, **kwargs)
29
+ super(protocol: protocol, **kwargs,
30
+ driver_info: Redis::LibIdentity.driver_info(kwargs[:driver_info]),
31
+ client_implementation: ::RedisClient)
29
32
  end
30
33
 
31
34
  def translate_error!(error, mapping: ERROR_MAPPING)
@@ -33,6 +36,24 @@ class Redis
33
36
  raise redis_error, error.message, error.backtrace
34
37
  end
35
38
 
39
+ # Whether +error+ raised during the connection handshake means the server can't speak RESP3
40
+ # (so we should retry the connection as RESP2). Covers Redis < 6 (no HELLO command, surfaced
41
+ # by redis-client as UnsupportedServer) and any server replying NOPROTO to `HELLO 3`.
42
+ def resp3_unsupported?(error)
43
+ return true if error.is_a?(::RedisClient::UnsupportedServer)
44
+ return true if error.is_a?(::RedisClient::CommandError) && error.message.include?("NOPROTO")
45
+
46
+ # Redis::Cluster discovers its topology by connecting to each startup node. When those nodes
47
+ # don't speak RESP3, redis-cluster-client collects the per-node failures and re-raises them
48
+ # wrapped in an InitialSetupError, discarding the original error classes (see
49
+ # RedisClient::Cluster::InitialSetupError.from_errors). Only the concatenated message
50
+ # survives, so match it to still trigger the RESP2 fallback for pre-6.0 clusters. Guarded by
51
+ # defined? because the cluster error class is only loaded with the redis-clustering gem.
52
+ defined?(::RedisClient::Cluster::InitialSetupError) &&
53
+ error.is_a?(::RedisClient::Cluster::InitialSetupError) &&
54
+ (error.message.include?("NOPROTO") || error.message.include?("HELLO command"))
55
+ end
56
+
36
57
  private
37
58
 
38
59
  def translate_error_class(error_class, mapping: ERROR_MAPPING)
@@ -62,6 +83,10 @@ class Redis
62
83
  config.db
63
84
  end
64
85
 
86
+ def protocol
87
+ config.protocol
88
+ end
89
+
65
90
  def host
66
91
  config.host unless config.path
67
92
  end
@@ -87,15 +112,16 @@ class Redis
87
112
  undef_method :call_once_v
88
113
  undef_method :blocking_call
89
114
 
90
- def ensure_connected(retryable: true, &block)
91
- super(retryable: retryable, &block)
92
- rescue ::RedisClient::Error => error
93
- Client.translate_error!(error)
94
- end
95
-
115
+ # We default to RESP3. Servers that can't speak it reject the `HELLO 3` handshake (Redis < 6.0
116
+ # has no HELLO at all; others answer NOPROTO). Re-raise those errors untranslated so they reach
117
+ # Redis#with_protocol_fallback as RedisClient::Error and trigger a transparent rebuild as RESP2 —
118
+ # the same path the sentinel and cluster clients already use. Everything else is translated to
119
+ # the matching Redis::* error.
96
120
  def call_v(command, &block)
97
121
  super(command, &block)
98
122
  rescue ::RedisClient::Error => error
123
+ raise if Client.resp3_unsupported?(error)
124
+
99
125
  Client.translate_error!(error)
100
126
  end
101
127
 
@@ -109,18 +135,24 @@ class Redis
109
135
 
110
136
  super(timeout, command, &block)
111
137
  rescue ::RedisClient::Error => error
138
+ raise if Client.resp3_unsupported?(error)
139
+
112
140
  Client.translate_error!(error)
113
141
  end
114
142
 
115
143
  def pipelined(exception: true)
116
144
  super
117
145
  rescue ::RedisClient::Error => error
146
+ raise if Client.resp3_unsupported?(error)
147
+
118
148
  Client.translate_error!(error)
119
149
  end
120
150
 
121
151
  def multi(watch: nil)
122
152
  super
123
153
  rescue ::RedisClient::Error => error
154
+ raise if Client.resp3_unsupported?(error)
155
+
124
156
  Client.translate_error!(error)
125
157
  end
126
158
 
@@ -7,9 +7,19 @@ class Redis
7
7
  #
8
8
  # @param [String] key
9
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])
10
+ # @param [Boolean] nx don't update already existing elements, always add new ones (since Redis 6.2)
11
+ # @param [Boolean] xx only update elements that already exist, never add new ones (since Redis 6.2)
12
+ # @param [Boolean] ch modify the return value to the number of changed elements (since Redis 6.2)
13
+ # @return [Integer] number of elements added to the sorted set, or changed when `ch` is set
14
+ def geoadd(key, *member, nx: false, xx: false, ch: false)
15
+ raise ArgumentError, "can't supply both nx and xx" if nx && xx
16
+
17
+ args = [:geoadd, key]
18
+ args << "NX" if nx
19
+ args << "XX" if xx
20
+ args << "CH" if ch
21
+ args.concat(member)
22
+ send_command(args)
13
23
  end
14
24
 
15
25
  # Returns geohash string representing position for specified members of the specified key.
@@ -28,6 +38,7 @@ class Redis
28
38
  # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest
29
39
  # or the farthest to the nearest relative to the center
30
40
  # @param [Integer] count limit the results to the first N matching items
41
+ # @param [Boolean] count_any return as soon as enough matches found (only with count, since Redis 6.2)
31
42
  # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
32
43
  # @return [Array<String>] may be changed with `options`
33
44
  def georadius(*args, **geoptions)
@@ -43,6 +54,7 @@ class Redis
43
54
  # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest or the farthest
44
55
  # to the nearest relative to the center
45
56
  # @param [Integer] count limit the results to the first N matching items
57
+ # @param [Boolean] count_any return as soon as enough matches found (only with count, since Redis 6.2)
46
58
  # @param ['WITHDIST', 'WITHCOORD', 'WITHHASH'] options to return additional information
47
59
  # @return [Array<String>] may be changed with `options`
48
60
  def georadiusbymember(*args, **geoptions)
@@ -61,6 +73,93 @@ class Redis
61
73
  send_command([:geopos, key, member])
62
74
  end
63
75
 
76
+ # Return the members of a geospatial sorted set that are within the borders of the
77
+ # area specified by a given shape, either a circle (BYRADIUS) or a rectangle (BYBOX),
78
+ # starting from a center point given either by member (FROMMEMBER) or by longitude and
79
+ # latitude (FROMLONLAT). Available since Redis 6.2.
80
+ #
81
+ # @example Search by radius from longitude/latitude
82
+ # redis.geosearch("Sicily", fromlonlat: [15, 37], byradius: [200, "km"], sort: "asc")
83
+ # # => ["Catania", "Palermo"]
84
+ #
85
+ # @example Search by box from an existing member, with extras
86
+ # redis.geosearch("Sicily", frommember: "Catania", bybox: [400, 400, "km"],
87
+ # sort: "asc", withcoord: true, withdist: true)
88
+ # # => [["Catania", "0.0000", ["15.087...", "37.502..."]], ...]
89
+ #
90
+ # @param [String] key
91
+ # @param [String] frommember use the position of the given existing member as the center
92
+ # @param [Array<Numeric>] fromlonlat a [longitude, latitude] pair used as the center
93
+ # @param [Array] byradius a [radius, unit] pair where unit is one of 'm', 'km', 'ft', 'mi'
94
+ # @param [Array] bybox a [width, height, unit] triple where unit is one of 'm', 'km', 'ft', 'mi'
95
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest, or vice versa
96
+ # @param [Integer] count limit the results to the first N matching items
97
+ # @param [Boolean] count_any return as soon as enough matches are found (only with count)
98
+ # @param [Boolean] withcoord also return the longitude and latitude of matching items
99
+ # @param [Boolean] withdist also return the distance from the center point
100
+ # @param [Boolean] withhash also return the raw geohash-encoded sorted set score of the item
101
+ # @return [Array<String>] may be changed with WITH* flags
102
+ def geosearch(key, frommember: nil, fromlonlat: nil, byradius: nil, bybox: nil,
103
+ sort: nil, count: nil, count_any: false,
104
+ withcoord: false, withdist: false, withhash: false)
105
+ args = [key]
106
+ args << "FROMMEMBER" << frommember if frommember
107
+ args << "FROMLONLAT" << fromlonlat[0] << fromlonlat[1] if fromlonlat
108
+ args << "BYRADIUS" << byradius[0] << byradius[1] if byradius
109
+ args << "BYBOX" << bybox[0] << bybox[1] << bybox[2] if bybox
110
+
111
+ options = []
112
+ options << "WITHCOORD" if withcoord
113
+ options << "WITHDIST" if withdist
114
+ options << "WITHHASH" if withhash
115
+
116
+ geoarguments = _geoarguments(*args, sort: sort, count: count, count_any: count_any, options: options)
117
+
118
+ send_command([:geosearch, *geoarguments])
119
+ end
120
+
121
+ # Like GEOSEARCH, but stores the result in a destination key. By default the destination
122
+ # is populated with the matching members and their geospatial scores; when STOREDIST is
123
+ # set, the members are stored with their distance from the center point as the score.
124
+ # Available since Redis 6.2.
125
+ #
126
+ # @example Store the three nearest members
127
+ # redis.geosearchstore("nearest", "Sicily",
128
+ # fromlonlat: [15, 37], bybox: [400, 400, "km"], sort: "asc", count: 3)
129
+ # # => 3
130
+ #
131
+ # @example Store distances as scores
132
+ # redis.geosearchstore("distances", "Sicily",
133
+ # fromlonlat: [15, 37], bybox: [400, 400, "km"], storedist: true)
134
+ # # => 3
135
+ #
136
+ # @param [String] destination key to store the result in
137
+ # @param [String] source geospatial sorted set to search
138
+ # @param [String] frommember use the position of the given existing member as the center
139
+ # @param [Array<Numeric>] fromlonlat a [longitude, latitude] pair used as the center
140
+ # @param [Array] byradius a [radius, unit] pair where unit is one of 'm', 'km', 'ft', 'mi'
141
+ # @param [Array] bybox a [width, height, unit] triple where unit is one of 'm', 'km', 'ft', 'mi'
142
+ # @param ['asc', 'desc'] sort sort returned items from the nearest to the farthest, or vice versa
143
+ # @param [Integer] count limit the results to the first N matching items
144
+ # @param [Boolean] count_any return as soon as enough matches are found (only with count)
145
+ # @param [Boolean] storedist store the distance from the center point as the score
146
+ # @return [Integer] number of elements stored in the destination key
147
+ def geosearchstore(destination, source, frommember: nil, fromlonlat: nil, byradius: nil, bybox: nil,
148
+ sort: nil, count: nil, count_any: false, storedist: false)
149
+ args = [destination, source]
150
+ args << "FROMMEMBER" << frommember if frommember
151
+ args << "FROMLONLAT" << fromlonlat[0] << fromlonlat[1] if fromlonlat
152
+ args << "BYRADIUS" << byradius[0] << byradius[1] if byradius
153
+ args << "BYBOX" << bybox[0] << bybox[1] << bybox[2] if bybox
154
+
155
+ options = []
156
+ options << "STOREDIST" if storedist
157
+
158
+ geoarguments = _geoarguments(*args, sort: sort, count: count, count_any: count_any, options: options)
159
+
160
+ send_command([:geosearchstore, *geoarguments])
161
+ end
162
+
64
163
  # Returns the distance between two members of a geospatial index
65
164
  #
66
165
  # @param [String ]key
@@ -73,10 +172,13 @@ class Redis
73
172
 
74
173
  private
75
174
 
76
- def _geoarguments(*args, options: nil, sort: nil, count: nil)
175
+ def _geoarguments(*args, options: nil, sort: nil, count: nil, count_any: false)
77
176
  args << sort if sort
78
- args << 'COUNT' << Integer(count) if count
79
- args << options if options
177
+ if count
178
+ args << 'COUNT' << Integer(count)
179
+ args << 'ANY' if count_any
180
+ end
181
+ args.concat(Array(options))
80
182
  args
81
183
  end
82
184
  end