Skip to content

Commit 3a3b6c3

Browse files
committed
Merge branch 'master' into vecsim-example
2 parents e1f1a85 + ef4caf5 commit 3a3b6c3

File tree

11 files changed

+290
-99
lines changed

11 files changed

+290
-99
lines changed

CHANGES

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11

22
* Add `items` parameter to `hset` signature
3-
* Create codeql-analysis.yml (#1988). Thanks @chayim
3+
* Create codeql-analysis.yml (#1988). Thanks @chayim
44
* Add limited support for Lua scripting with RedisCluster
55
* Implement `.lock()` method on RedisCluster
6+
* Fix cursor returned by SCAN for RedisCluster & change default target to PRIMARIES
7+
* Fix scan_iter for RedisCluster
8+
* Remove verbose logging when initializing ClusterPubSub, ClusterPipeline or RedisCluster
69

710
* 4.1.3 (Feb 8, 2022)
811
* Fix flushdb and flushall (#1926)

redis/cluster.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import time
88
from collections import OrderedDict
99

10-
from redis.client import CaseInsensitiveDict, PubSub, Redis
10+
from redis.client import CaseInsensitiveDict, PubSub, Redis, parse_scan
1111
from redis.commands import CommandsParser, RedisClusterCommands
1212
from redis.connection import ConnectionPool, DefaultParser, Encoder, parse_url
1313
from redis.crc import REDIS_CLUSTER_HASH_SLOTS, key_slot
@@ -51,10 +51,14 @@ def get_connection(redis_node, *args, **options):
5151

5252

5353
def parse_scan_result(command, res, **options):
54-
keys_list = []
55-
for primary_res in res.values():
56-
keys_list += primary_res[1]
57-
return 0, keys_list
54+
cursors = {}
55+
ret = []
56+
for node_name, response in res.items():
57+
cursor, r = parse_scan(response, **options)
58+
cursors[node_name] = cursor
59+
ret += r
60+
61+
return cursors, ret
5862

5963

6064
def parse_pubsub_numsub(command, res, **options):
@@ -244,7 +248,6 @@ class RedisCluster(RedisClusterCommands):
244248
"INFO",
245249
"SHUTDOWN",
246250
"KEYS",
247-
"SCAN",
248251
"DBSIZE",
249252
"BGSAVE",
250253
"SLOWLOG GET",
@@ -298,6 +301,7 @@ class RedisCluster(RedisClusterCommands):
298301
"FUNCTION LIST",
299302
"FUNCTION LOAD",
300303
"FUNCTION RESTORE",
304+
"SCAN",
301305
"SCRIPT EXISTS",
302306
"SCRIPT FLUSH",
303307
"SCRIPT LOAD",
@@ -518,8 +522,6 @@ def __init__(
518522
RedisClusterException:
519523
- db (Redis do not support database SELECT in cluster mode)
520524
"""
521-
log.info("Creating a new instance of RedisCluster client")
522-
523525
if startup_nodes is None:
524526
startup_nodes = []
525527

@@ -1670,7 +1672,6 @@ def __init__(self, redis_cluster, node=None, host=None, port=None, **kwargs):
16701672
:type host: str
16711673
:type port: int
16721674
"""
1673-
log.info("Creating new instance of ClusterPubSub")
16741675
self.node = None
16751676
self.set_pubsub_node(redis_cluster, node, host, port)
16761677
connection_pool = (
@@ -1802,7 +1803,6 @@ def __init__(
18021803
**kwargs,
18031804
):
18041805
""" """
1805-
log.info("Creating new instance of ClusterPipeline")
18061806
self.command_stack = []
18071807
self.nodes_manager = nodes_manager
18081808
self.commands_parser = commands_parser

redis/commands/cluster.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
1+
from typing import Iterator, Union
2+
13
from redis.crc import key_slot
24
from redis.exceptions import RedisClusterException, RedisError
5+
from redis.typing import PatternT
36

47
from .core import (
58
ACLCommands,
@@ -206,6 +209,41 @@ def stralgo(
206209
**kwargs,
207210
)
208211

212+
def scan_iter(
213+
self,
214+
match: Union[PatternT, None] = None,
215+
count: Union[int, None] = None,
216+
_type: Union[str, None] = None,
217+
**kwargs,
218+
) -> Iterator:
219+
# Do the first query with cursor=0 for all nodes
220+
cursors, data = self.scan(match=match, count=count, _type=_type, **kwargs)
221+
yield from data
222+
223+
cursors = {name: cursor for name, cursor in cursors.items() if cursor != 0}
224+
if cursors:
225+
# Get nodes by name
226+
nodes = {name: self.get_node(node_name=name) for name in cursors.keys()}
227+
228+
# Iterate over each node till its cursor is 0
229+
kwargs.pop("target_nodes", None)
230+
while cursors:
231+
for name, cursor in cursors.items():
232+
cur, data = self.scan(
233+
cursor=cursor,
234+
match=match,
235+
count=count,
236+
_type=_type,
237+
target_nodes=nodes[name],
238+
**kwargs,
239+
)
240+
yield from data
241+
cursors[name] = cur[name]
242+
243+
cursors = {
244+
name: cursor for name, cursor in cursors.items() if cursor != 0
245+
}
246+
209247

210248
class RedisClusterCommands(
211249
ClusterMultiKeyCommands,

redis/commands/search/field.py

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,30 @@
1+
from typing import List
2+
3+
from redis import DataError
4+
5+
16
class Field:
27

38
NUMERIC = "NUMERIC"
49
TEXT = "TEXT"
510
WEIGHT = "WEIGHT"
611
GEO = "GEO"
712
TAG = "TAG"
13+
VECTOR = "VECTOR"
814
SORTABLE = "SORTABLE"
915
NOINDEX = "NOINDEX"
1016
AS = "AS"
1117

12-
def __init__(self, name, args=[], sortable=False, no_index=False, as_name=None):
18+
def __init__(
19+
self,
20+
name: str,
21+
args: List[str] = None,
22+
sortable: bool = False,
23+
no_index: bool = False,
24+
as_name: str = None,
25+
):
26+
if args is None:
27+
args = []
1328
self.name = name
1429
self.args = args
1530
self.args_suffix = list()
@@ -44,7 +59,12 @@ class TextField(Field):
4459
PHONETIC = "PHONETIC"
4560

4661
def __init__(
47-
self, name, weight=1.0, no_stem=False, phonetic_matcher=None, **kwargs
62+
self,
63+
name: str,
64+
weight: float = 1.0,
65+
no_stem: bool = False,
66+
phonetic_matcher: str = None,
67+
**kwargs,
4868
):
4969
Field.__init__(self, name, args=[Field.TEXT, Field.WEIGHT, weight], **kwargs)
5070

@@ -65,7 +85,7 @@ class NumericField(Field):
6585
NumericField is used to define a numeric field in a schema definition
6686
"""
6787

68-
def __init__(self, name, **kwargs):
88+
def __init__(self, name: str, **kwargs):
6989
Field.__init__(self, name, args=[Field.NUMERIC], **kwargs)
7090

7191

@@ -74,7 +94,7 @@ class GeoField(Field):
7494
GeoField is used to define a geo-indexing field in a schema definition
7595
"""
7696

77-
def __init__(self, name, **kwargs):
97+
def __init__(self, name: str, **kwargs):
7898
Field.__init__(self, name, args=[Field.GEO], **kwargs)
7999

80100

@@ -86,7 +106,52 @@ class TagField(Field):
86106

87107
SEPARATOR = "SEPARATOR"
88108

89-
def __init__(self, name, separator=",", **kwargs):
109+
def __init__(self, name: str, separator: str = ",", **kwargs):
90110
Field.__init__(
91111
self, name, args=[Field.TAG, self.SEPARATOR, separator], **kwargs
92112
)
113+
114+
115+
class VectorField(Field):
116+
"""
117+
Allows vector similarity queries against the value in this attribute.
118+
See https://oss.redis.com/redisearch/Vectors/#vector_fields.
119+
"""
120+
121+
def __init__(self, name: str, algorithm: str, attributes: dict, **kwargs):
122+
"""
123+
Create Vector Field. Notice that Vector cannot have sortable or no_index tag,
124+
although it's also a Field.
125+
126+
``name`` is the name of the field.
127+
128+
``algorithm`` can be "FLAT" or "HNSW".
129+
130+
``attributes`` each algorithm can have specific attributes. Some of them
131+
are mandatory and some of them are optional. See
132+
https://oss.redis.com/redisearch/master/Vectors/#specific_creation_attributes_per_algorithm
133+
for more information.
134+
"""
135+
sort = kwargs.get("sortable", False)
136+
noindex = kwargs.get("no_index", False)
137+
138+
if sort or noindex:
139+
raise DataError("Cannot set 'sortable' or 'no_index' in Vector fields.")
140+
141+
if algorithm.upper() not in ["FLAT", "HNSW"]:
142+
raise DataError(
143+
"Realtime vector indexing supporting 2 Indexing Methods:"
144+
"'FLAT' and 'HNSW'."
145+
)
146+
147+
attr_li = []
148+
149+
for key, value in attributes.items():
150+
attr_li.extend([key, value])
151+
152+
Field.__init__(
153+
self,
154+
name,
155+
args=[Field.VECTOR, algorithm, len(attr_li), *attr_li],
156+
**kwargs,
157+
)

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
long_description_content_type="text/markdown",
99
keywords=["Redis", "key-value store", "database"],
1010
license="MIT",
11-
version="4.2.0rc3",
11+
version="4.2.0",
1212
packages=find_packages(
1313
include=[
1414
"redis",

tests/test_bloom.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ def test_create(client):
3434
assert client.cms().initbydim("cmsDim", 100, 5)
3535
assert client.cms().initbyprob("cmsProb", 0.01, 0.01)
3636
assert client.topk().reserve("topk", 5, 100, 5, 0.9)
37+
38+
39+
@pytest.mark.redismod
40+
@pytest.mark.experimental
41+
def test_tdigest_create(client):
3742
assert client.tdigest().create("tDigest", 100)
3843

3944

@@ -306,6 +311,7 @@ def test_topk_incrby(client):
306311

307312
# region Test T-Digest
308313
@pytest.mark.redismod
314+
@pytest.mark.experimental
309315
def test_tdigest_reset(client):
310316
assert client.tdigest().create("tDigest", 10)
311317
# reset on empty histogram
@@ -319,6 +325,7 @@ def test_tdigest_reset(client):
319325

320326

321327
@pytest.mark.redismod
328+
@pytest.mark.experimental
322329
def test_tdigest_merge(client):
323330
assert client.tdigest().create("to-tDigest", 10)
324331
assert client.tdigest().create("from-tDigest", 10)
@@ -334,6 +341,7 @@ def test_tdigest_merge(client):
334341

335342

336343
@pytest.mark.redismod
344+
@pytest.mark.experimental
337345
def test_tdigest_min_and_max(client):
338346
assert client.tdigest().create("tDigest", 100)
339347
# insert data-points into sketch
@@ -344,6 +352,7 @@ def test_tdigest_min_and_max(client):
344352

345353

346354
@pytest.mark.redismod
355+
@pytest.mark.experimental
347356
def test_tdigest_quantile(client):
348357
assert client.tdigest().create("tDigest", 500)
349358
# insert data-points into sketch
@@ -359,6 +368,7 @@ def test_tdigest_quantile(client):
359368

360369

361370
@pytest.mark.redismod
371+
@pytest.mark.experimental
362372
def test_tdigest_cdf(client):
363373
assert client.tdigest().create("tDigest", 100)
364374
# insert data-points into sketch

tests/test_cluster.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1773,29 +1773,60 @@ def test_cluster_scan(self, r):
17731773
r.set("a", 1)
17741774
r.set("b", 2)
17751775
r.set("c", 3)
1776-
cursor, keys = r.scan(target_nodes="primaries")
1777-
assert cursor == 0
1778-
assert set(keys) == {b"a", b"b", b"c"}
1779-
_, keys = r.scan(match="a", target_nodes="primaries")
1780-
assert set(keys) == {b"a"}
1776+
1777+
for target_nodes, nodes in zip(
1778+
["primaries", "replicas"], [r.get_primaries(), r.get_replicas()]
1779+
):
1780+
cursors, keys = r.scan(target_nodes=target_nodes)
1781+
assert sorted(keys) == [b"a", b"b", b"c"]
1782+
assert sorted(cursors.keys()) == sorted(node.name for node in nodes)
1783+
assert all(cursor == 0 for cursor in cursors.values())
1784+
1785+
cursors, keys = r.scan(match="a*", target_nodes=target_nodes)
1786+
assert sorted(keys) == [b"a"]
1787+
assert sorted(cursors.keys()) == sorted(node.name for node in nodes)
1788+
assert all(cursor == 0 for cursor in cursors.values())
17811789

17821790
@skip_if_server_version_lt("6.0.0")
17831791
def test_cluster_scan_type(self, r):
17841792
r.sadd("a-set", 1)
1793+
r.sadd("b-set", 1)
1794+
r.sadd("c-set", 1)
17851795
r.hset("a-hash", "foo", 2)
17861796
r.lpush("a-list", "aux", 3)
1787-
_, keys = r.scan(match="a*", _type="SET", target_nodes="primaries")
1788-
assert set(keys) == {b"a-set"}
1797+
1798+
for target_nodes, nodes in zip(
1799+
["primaries", "replicas"], [r.get_primaries(), r.get_replicas()]
1800+
):
1801+
cursors, keys = r.scan(_type="SET", target_nodes=target_nodes)
1802+
assert sorted(keys) == [b"a-set", b"b-set", b"c-set"]
1803+
assert sorted(cursors.keys()) == sorted(node.name for node in nodes)
1804+
assert all(cursor == 0 for cursor in cursors.values())
1805+
1806+
cursors, keys = r.scan(_type="SET", match="a*", target_nodes=target_nodes)
1807+
assert sorted(keys) == [b"a-set"]
1808+
assert sorted(cursors.keys()) == sorted(node.name for node in nodes)
1809+
assert all(cursor == 0 for cursor in cursors.values())
17891810

17901811
@skip_if_server_version_lt("2.8.0")
17911812
def test_cluster_scan_iter(self, r):
1792-
r.set("a", 1)
1793-
r.set("b", 2)
1794-
r.set("c", 3)
1795-
keys = list(r.scan_iter(target_nodes="primaries"))
1796-
assert set(keys) == {b"a", b"b", b"c"}
1797-
keys = list(r.scan_iter(match="a", target_nodes="primaries"))
1798-
assert set(keys) == {b"a"}
1813+
keys_all = []
1814+
keys_1 = []
1815+
for i in range(100):
1816+
s = str(i)
1817+
r.set(s, 1)
1818+
keys_all.append(s.encode("utf-8"))
1819+
if s.startswith("1"):
1820+
keys_1.append(s.encode("utf-8"))
1821+
keys_all.sort()
1822+
keys_1.sort()
1823+
1824+
for target_nodes in ["primaries", "replicas"]:
1825+
keys = r.scan_iter(target_nodes=target_nodes)
1826+
assert sorted(keys) == keys_all
1827+
1828+
keys = r.scan_iter(match="1*", target_nodes=target_nodes)
1829+
assert sorted(keys) == keys_1
17991830

18001831
def test_cluster_randomkey(self, r):
18011832
node = r.get_node_from_key("{foo}")

0 commit comments

Comments
 (0)