redis/modules/vector-sets/tests/vrange.py
Salvatore Sanfilippo 3de2fdad58
[vector sets] VRANGE implementation (#14235)
This is basically the Vector Set iteration primitive.
It exploits the underlying radix tree implementation.
The usage pattern is strongly reminiscent of other Redis commands doing
similar things.

The command usage is straightforward:

```
> VRANGE word_embeddings_int8 [Redis + 10
 1) "Redis"
 2) "Rediscover"
 3) "Rediscover_Ashland"
 4) "Rediscover_Northern_Ireland"
 5) "Rediscovered"
 6) "Rediscovered_Bookshop"
 7) "Rediscovering"
 8) "Rediscovering_God"
 9) "Rediscovering_Lost"
10) "Rediscovers"
```

The above command returns 10 (or less, if less are available in the
specified range) elements from "Redis" (inclusive) to the maximum
possible element. The comparison is performed byte by byte, as
`memcmp()` would do, in this way the elements have a total order. The
start and end range can be either a string, prefixed
by `[` or `(` (the prefix is mandatory) to tell the command if the range
is inclusive or exclusive, or can be the special symbols `-` and `+`
that means the maximum and minimum element.

More info can be found in the implementation itself and in the README
file change.

---------

Co-authored-by: debing.sun <debing.sun@redis.com>
2025-10-09 10:14:14 +08:00

113 lines
5.5 KiB
Python

from test import TestCase, generate_random_vector
import struct
class BasicVRANGE(TestCase):
def getname(self):
return "VRANGE basic functionality and iteration"
def test(self):
# Add multiple elements with different names for lexicographical ordering
elements = [
"apple", "apricot", "banana", "cherry", "date",
"elderberry", "fig", "grape", "honeydew", "kiwi",
"lemon", "mango", "nectarine", "orange", "papaya",
"quince", "raspberry", "strawberry", "tangerine", "watermelon"
]
# Add all elements to the vector set
for elem in elements:
vec = generate_random_vector(4)
vec_bytes = struct.pack('4f', *vec)
self.redis.execute_command('VADD', self.test_key, 'FP32', vec_bytes, elem)
# Test 1: Basic range with inclusive boundaries
result = self.redis.execute_command('VRANGE', self.test_key, '[apple', '[grape', '5')
result = [r.decode() for r in result]
assert result == ['apple', 'apricot', 'banana', 'cherry', 'date'], f"Expected first 5 elements from apple, got {result}"
# Test 2: Exclusive start boundary
result = self.redis.execute_command('VRANGE', self.test_key, '(apple', '[cherry', '10')
result = [r.decode() for r in result]
assert result == ['apricot', 'banana', 'cherry'], f"Expected elements after apple up to cherry inclusive, got {result}"
# Test 3: Exclusive end boundary
result = self.redis.execute_command('VRANGE', self.test_key, '[banana', '(cherry', '10')
result = [r.decode() for r in result]
assert result == ['banana'], f"Expected only banana (cherry excluded), got {result}"
# Test 4: Using '-' for minimum element
result = self.redis.execute_command('VRANGE', self.test_key, '-', '[banana', '10')
result = [r.decode() for r in result]
assert result[0] == 'apple', "Should start from the first element"
assert result[-1] == 'banana', "Should end at banana"
# Test 5: Using '+' for maximum element
result = self.redis.execute_command('VRANGE', self.test_key, '[raspberry', '+', '10')
result = [r.decode() for r in result]
assert 'raspberry' in result and 'strawberry' in result and 'tangerine' in result and 'watermelon' in result, "Should include all elements from raspberry onwards"
# Test 6: Full range with '-' and '+'
result = self.redis.execute_command('VRANGE', self.test_key, '-', '+', '100')
result = [r.decode() for r in result]
assert len(result) == len(elements), f"Should return all {len(elements)} elements"
assert result == sorted(elements), "Elements should be in lexicographical order"
# Test 7: Iterator pattern - verify each element appears exactly once
seen = set()
batch_size = 3
current = '-'
while True:
if current == '-':
# First iteration
result = self.redis.execute_command('VRANGE', self.test_key, '-', '+', str(batch_size))
else:
# Subsequent iterations - exclusive start from last element
result = self.redis.execute_command('VRANGE', self.test_key, f'({current}', '+', str(batch_size))
result = [r.decode() for r in result]
if not result:
break
# Check no duplicates in this batch
for elem in result:
assert elem not in seen, f"Element {elem} appeared more than once"
seen.add(elem)
# Update current to last element
current = result[-1]
# Break if we got less than requested (end of set)
if len(result) < batch_size:
break
# Verify we saw all elements exactly once
assert seen == set(elements), f"Iterator should visit all elements exactly once. Missing: {set(elements) - seen}, Extra: {seen - set(elements)}"
# Test 8: Count of 0 returns empty array
result = self.redis.execute_command('VRANGE', self.test_key, '-', '+', '0')
assert result == [], f"Count of 0 should return empty array, got {result}"
# Test 9: Range with no matching elements
result = self.redis.execute_command('VRANGE', self.test_key, '[zebra', '+', '10')
assert result == [], f"Range beyond all elements should return empty array, got {result}"
# Test 10: Non-existent key
result = self.redis.execute_command('VRANGE', 'nonexistent_key', '-', '+', '10')
assert result == [], f"Non-existent key should return empty array, got {result}"
# Test 11: Partial word boundaries
result = self.redis.execute_command('VRANGE', self.test_key, '[app', '[apr', '10')
result = [r.decode() for r in result]
assert 'apple' in result, "Should include 'apple' which starts with 'app'"
assert 'apricot' not in result, "Should not include 'apricot' as it's >= 'apr'"
# Test 12: Single element range
result = self.redis.execute_command('VRANGE', self.test_key, '[cherry', '[cherry', '10')
result = [r.decode() for r in result]
assert result == ['cherry'], f"Inclusive single element range should return that element, got {result}"
# Test 13: Empty range (start > end)
result = self.redis.execute_command('VRANGE', self.test_key, '[grape', '[apple', '10')
assert result == [], f"Range where start > end should return empty array, got {result}"