Skip to content

Commit faf917a

Browse files
authored
Adding support for vrange command. (#3927)
1 parent f10a07a commit faf917a

3 files changed

Lines changed: 323 additions & 0 deletions

File tree

redis/commands/vectorset/commands.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
VSETATTR_CMD = "VSETATTR"
1919
VGETATTR_CMD = "VGETATTR"
2020
VRANDMEMBER_CMD = "VRANDMEMBER"
21+
VRANGE_CMD = "VRANGE"
2122

2223
# Return type for vsim command
2324
VSimResult = Optional[
@@ -390,3 +391,33 @@ def vrandmember(
390391
if count is not None:
391392
pieces.append(count)
392393
return self.execute_command(VRANDMEMBER_CMD, *pieces)
394+
395+
def vrange(
396+
self, key: KeyT, start: str, end: str, count: Optional[int] = None
397+
) -> Union[Awaitable[List[str]], List[str]]:
398+
"""
399+
Return elements in a lexicographical range from a vector set ``key``.
400+
401+
``start`` is the starting point of the lexicographical range. Can be:
402+
- A string prefixed with '[' for inclusive range (e.g., '[Redis')
403+
- A string prefixed with '(' for exclusive range (e.g., '(a7')
404+
- The special symbol '-' to indicate the minimum element
405+
406+
``end`` is the ending point of the lexicographical range. Can be:
407+
- A string prefixed with '[' for inclusive range
408+
- A string prefixed with '(' for exclusive range
409+
- The special symbol '+' to indicate the maximum element
410+
411+
``count`` is the maximum number of elements to return.
412+
If ``count`` is not provided or negative, all elements in the range are returned.
413+
If ``count`` is positive, at most ``count`` elements are returned.
414+
415+
Returns an array of elements in lexicographical order within the specified range.
416+
Returns an empty array if the key doesn't exist.
417+
418+
For more information, see https://redis.io/commands/vrange.
419+
"""
420+
pieces = [key, start, end]
421+
if count is not None:
422+
pieces.append(count)
423+
return self.execute_command(VRANGE_CMD, *pieces)

tests/test_asyncio/test_vsets.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -994,3 +994,149 @@ def _validate_quantization(original, quantized, tolerance=0.1):
994994
return False
995995
else:
996996
return True
997+
998+
999+
@skip_if_server_version_lt("8.4.0")
1000+
async def test_vrange_basic(d_client):
1001+
"""Test basic VRANGE functionality with lexicographical ordering."""
1002+
# Add elements with different names
1003+
elements = ["apple", "banana", "cherry", "date", "elderberry"]
1004+
for elem in elements:
1005+
await d_client.vset().vadd("myset", [1.0, 2.0, 3.0], elem)
1006+
1007+
# Test full range
1008+
result = await d_client.vset().vrange("myset", "-", "+")
1009+
assert result == elements
1010+
assert len(result) == 5
1011+
1012+
# Test inclusive range
1013+
result = await d_client.vset().vrange("myset", "[banana", "[date")
1014+
assert result == ["banana", "cherry", "date"]
1015+
1016+
# Test exclusive range
1017+
result = await d_client.vset().vrange("myset", "(banana", "(date")
1018+
assert result == ["cherry"]
1019+
1020+
1021+
@skip_if_server_version_lt("8.4.0")
1022+
async def test_vrange_with_count(d_client):
1023+
"""Test VRANGE with count parameter."""
1024+
# Add elements
1025+
elements = ["a", "b", "c", "d", "e", "f", "g"]
1026+
for elem in elements:
1027+
await d_client.vset().vadd("myset", [1.0, 2.0], elem)
1028+
1029+
# Test with positive count
1030+
result = await d_client.vset().vrange("myset", "-", "+", count=3)
1031+
assert len(result) == 3
1032+
assert result == ["a", "b", "c"]
1033+
1034+
# Test with count larger than set size
1035+
result = await d_client.vset().vrange("myset", "-", "+", count=100)
1036+
assert len(result) == 7
1037+
assert result == elements
1038+
1039+
# Test with count = 0
1040+
result = await d_client.vset().vrange("myset", "-", "+", count=0)
1041+
assert result == []
1042+
1043+
# Test with negative count (should return all)
1044+
result = await d_client.vset().vrange("myset", "-", "+", count=-1)
1045+
assert len(result) == 7
1046+
assert result == elements
1047+
1048+
1049+
@skip_if_server_version_lt("8.4.0")
1050+
async def test_vrange_iteration(d_client):
1051+
"""Test VRANGE for stateless iteration."""
1052+
# Add elements
1053+
elements = [f"elem{i:03d}" for i in range(20)]
1054+
for elem in elements:
1055+
await d_client.vset().vadd("myset", [1.0, 2.0], elem)
1056+
1057+
# Iterate through all elements, 5 at a time
1058+
all_results = []
1059+
start = "-"
1060+
while True:
1061+
result = await d_client.vset().vrange("myset", start, "+", count=5)
1062+
if not result:
1063+
break
1064+
all_results.extend(result)
1065+
# Continue from the last element (exclusive)
1066+
start = f"({result[-1]}"
1067+
1068+
assert len(all_results) == 20
1069+
assert all_results == elements
1070+
1071+
1072+
@skip_if_server_version_lt("8.4.0")
1073+
async def test_vrange_empty_key(d_client):
1074+
"""Test VRANGE on non-existent key."""
1075+
result = await d_client.vset().vrange("nonexistent", "-", "+")
1076+
assert result == []
1077+
1078+
result = await d_client.vset().vrange("nonexistent", "[a", "[z", count=10)
1079+
assert result == []
1080+
1081+
1082+
@skip_if_server_version_lt("8.4.0")
1083+
async def test_vrange_special_characters(d_client):
1084+
"""Test VRANGE with elements containing special characters."""
1085+
# Add elements with special characters
1086+
elements = ["a:1", "a:2", "b:1", "b:2", "c:1"]
1087+
for elem in elements:
1088+
await d_client.vset().vadd("myset", [1.0, 2.0], elem)
1089+
1090+
# Test range with prefix
1091+
result = await d_client.vset().vrange("myset", "[a:", "[a:9")
1092+
assert result == ["a:1", "a:2"]
1093+
1094+
result = await d_client.vset().vrange("myset", "[b:", "[b:9")
1095+
assert result == ["b:1", "b:2"]
1096+
1097+
1098+
@skip_if_server_version_lt("8.4.0")
1099+
async def test_vrange_single_element(d_client):
1100+
"""Test VRANGE with a single element."""
1101+
await d_client.vset().vadd("myset", [1.0, 2.0], "single")
1102+
1103+
result = await d_client.vset().vrange("myset", "-", "+")
1104+
assert result == ["single"]
1105+
1106+
result = await d_client.vset().vrange("myset", "[single", "[single")
1107+
assert result == ["single"]
1108+
1109+
result = await d_client.vset().vrange("myset", "(single", "+")
1110+
assert result == []
1111+
1112+
1113+
@skip_if_server_version_lt("8.4.0")
1114+
async def test_vrange_lexicographical_order(d_client):
1115+
"""Test that VRANGE returns elements in correct lexicographical order."""
1116+
# Add elements in random order
1117+
elements = ["zebra", "apple", "mango", "banana", "cherry"]
1118+
for elem in elements:
1119+
await d_client.vset().vadd("myset", [1.0, 2.0], elem)
1120+
1121+
# Should return in sorted order
1122+
result = await d_client.vset().vrange("myset", "-", "+")
1123+
expected = sorted(elements)
1124+
assert result == expected
1125+
1126+
1127+
@skip_if_server_version_lt("8.4.0")
1128+
async def test_vrange_numeric_strings(d_client):
1129+
"""Test VRANGE with numeric string elements."""
1130+
# Add numeric strings (lexicographical order, not numeric)
1131+
elements = ["1", "10", "2", "20", "3"]
1132+
for elem in elements:
1133+
await d_client.vset().vadd("myset", [1.0, 2.0], elem)
1134+
1135+
# Lexicographical order: "1", "10", "2", "20", "3"
1136+
result = await d_client.vset().vrange("myset", "-", "+")
1137+
expected = sorted(elements) # ["1", "10", "2", "20", "3"]
1138+
assert result == expected
1139+
1140+
# Range from "1" to "2" (inclusive)
1141+
result = await d_client.vset().vrange("myset", "[1", "[2")
1142+
assert result == ["1", "10", "2"]

tests/test_vsets.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -990,3 +990,149 @@ def _validate_quantization(original, quantized, tolerance=0.1):
990990
return False
991991
else:
992992
return True
993+
994+
995+
@skip_if_server_version_lt("8.4.0")
996+
def test_vrange_basic(d_client):
997+
"""Test basic VRANGE functionality with lexicographical ordering."""
998+
# Add elements with different names
999+
elements = ["apple", "banana", "cherry", "date", "elderberry"]
1000+
for elem in elements:
1001+
d_client.vset().vadd("myset", [1.0, 2.0, 3.0], elem)
1002+
1003+
# Test full range
1004+
result = d_client.vset().vrange("myset", "-", "+")
1005+
assert result == elements
1006+
assert len(result) == 5
1007+
1008+
# Test inclusive range
1009+
result = d_client.vset().vrange("myset", "[banana", "[date")
1010+
assert result == ["banana", "cherry", "date"]
1011+
1012+
# Test exclusive range
1013+
result = d_client.vset().vrange("myset", "(banana", "(date")
1014+
assert result == ["cherry"]
1015+
1016+
1017+
@skip_if_server_version_lt("8.4.0")
1018+
def test_vrange_with_count(d_client):
1019+
"""Test VRANGE with count parameter."""
1020+
# Add elements
1021+
elements = ["a", "b", "c", "d", "e", "f", "g"]
1022+
for elem in elements:
1023+
d_client.vset().vadd("myset", [1.0, 2.0], elem)
1024+
1025+
# Test with positive count
1026+
result = d_client.vset().vrange("myset", "-", "+", count=3)
1027+
assert len(result) == 3
1028+
assert result == ["a", "b", "c"]
1029+
1030+
# Test with count larger than set size
1031+
result = d_client.vset().vrange("myset", "-", "+", count=100)
1032+
assert len(result) == 7
1033+
assert result == elements
1034+
1035+
# Test with count = 0
1036+
result = d_client.vset().vrange("myset", "-", "+", count=0)
1037+
assert result == []
1038+
1039+
# Test with negative count (should return all)
1040+
result = d_client.vset().vrange("myset", "-", "+", count=-1)
1041+
assert len(result) == 7
1042+
assert result == elements
1043+
1044+
1045+
@skip_if_server_version_lt("8.4.0")
1046+
def test_vrange_iteration(d_client):
1047+
"""Test VRANGE for stateless iteration."""
1048+
# Add elements
1049+
elements = [f"elem{i:03d}" for i in range(20)]
1050+
for elem in elements:
1051+
d_client.vset().vadd("myset", [1.0, 2.0], elem)
1052+
1053+
# Iterate through all elements, 5 at a time
1054+
all_results = []
1055+
start = "-"
1056+
while True:
1057+
result = d_client.vset().vrange("myset", start, "+", count=5)
1058+
if not result:
1059+
break
1060+
all_results.extend(result)
1061+
# Continue from the last element (exclusive)
1062+
start = f"({result[-1]}"
1063+
1064+
assert len(all_results) == 20
1065+
assert all_results == elements
1066+
1067+
1068+
@skip_if_server_version_lt("8.4.0")
1069+
def test_vrange_empty_key(d_client):
1070+
"""Test VRANGE on non-existent key."""
1071+
result = d_client.vset().vrange("nonexistent", "-", "+")
1072+
assert result == []
1073+
1074+
result = d_client.vset().vrange("nonexistent", "[a", "[z", count=10)
1075+
assert result == []
1076+
1077+
1078+
@skip_if_server_version_lt("8.4.0")
1079+
def test_vrange_special_characters(d_client):
1080+
"""Test VRANGE with elements containing special characters."""
1081+
# Add elements with special characters
1082+
elements = ["a:1", "a:2", "b:1", "b:2", "c:1"]
1083+
for elem in elements:
1084+
d_client.vset().vadd("myset", [1.0, 2.0], elem)
1085+
1086+
# Test range with prefix
1087+
result = d_client.vset().vrange("myset", "[a:", "[a:9")
1088+
assert result == ["a:1", "a:2"]
1089+
1090+
result = d_client.vset().vrange("myset", "[b:", "[b:9")
1091+
assert result == ["b:1", "b:2"]
1092+
1093+
1094+
@skip_if_server_version_lt("8.4.0")
1095+
def test_vrange_single_element(d_client):
1096+
"""Test VRANGE with a single element."""
1097+
d_client.vset().vadd("myset", [1.0, 2.0], "single")
1098+
1099+
result = d_client.vset().vrange("myset", "-", "+")
1100+
assert result == ["single"]
1101+
1102+
result = d_client.vset().vrange("myset", "[single", "[single")
1103+
assert result == ["single"]
1104+
1105+
result = d_client.vset().vrange("myset", "(single", "+")
1106+
assert result == []
1107+
1108+
1109+
@skip_if_server_version_lt("8.4.0")
1110+
def test_vrange_lexicographical_order(d_client):
1111+
"""Test that VRANGE returns elements in correct lexicographical order."""
1112+
# Add elements in random order
1113+
elements = ["zebra", "apple", "mango", "banana", "cherry"]
1114+
for elem in elements:
1115+
d_client.vset().vadd("myset", [1.0, 2.0], elem)
1116+
1117+
# Should return in sorted order
1118+
result = d_client.vset().vrange("myset", "-", "+")
1119+
expected = sorted(elements)
1120+
assert result == expected
1121+
1122+
1123+
@skip_if_server_version_lt("8.4.0")
1124+
def test_vrange_numeric_strings(d_client):
1125+
"""Test VRANGE with numeric string elements."""
1126+
# Add numeric strings (lexicographical order, not numeric)
1127+
elements = ["1", "10", "2", "20", "3"]
1128+
for elem in elements:
1129+
d_client.vset().vadd("myset", [1.0, 2.0], elem)
1130+
1131+
# Lexicographical order: "1", "10", "2", "20", "3"
1132+
result = d_client.vset().vrange("myset", "-", "+")
1133+
expected = sorted(elements) # ["1", "10", "2", "20", "3"]
1134+
assert result == expected
1135+
1136+
# Range from "1" to "2" (inclusive)
1137+
result = d_client.vset().vrange("myset", "[1", "[2")
1138+
assert result == ["1", "10", "2"]

0 commit comments

Comments
 (0)