diff options
Diffstat (limited to 'examples/redis-unstable/modules/vector-sets/tests/with.py')
| -rw-r--r-- | examples/redis-unstable/modules/vector-sets/tests/with.py | 214 |
1 files changed, 0 insertions, 214 deletions
diff --git a/examples/redis-unstable/modules/vector-sets/tests/with.py b/examples/redis-unstable/modules/vector-sets/tests/with.py deleted file mode 100644 index d14a23f..0000000 --- a/examples/redis-unstable/modules/vector-sets/tests/with.py +++ /dev/null | |||
| @@ -1,214 +0,0 @@ | |||
| 1 | from test import TestCase, generate_random_vector | ||
| 2 | import struct | ||
| 3 | import json | ||
| 4 | import random | ||
| 5 | |||
| 6 | class VSIMWithAttribs(TestCase): | ||
| 7 | def getname(self): | ||
| 8 | return "VSIM WITHATTRIBS/WITHSCORES functionality testing" | ||
| 9 | |||
| 10 | def setup(self): | ||
| 11 | super().setup() | ||
| 12 | self.dim = 8 | ||
| 13 | self.count = 20 | ||
| 14 | |||
| 15 | # Create vectors with attributes | ||
| 16 | for i in range(self.count): | ||
| 17 | vec = generate_random_vector(self.dim) | ||
| 18 | vec_bytes = struct.pack(f'{self.dim}f', *vec) | ||
| 19 | |||
| 20 | # Item name | ||
| 21 | name = f"{self.test_key}:item:{i}" | ||
| 22 | |||
| 23 | # Add to Redis | ||
| 24 | self.redis.execute_command('VADD', self.test_key, 'FP32', vec_bytes, name) | ||
| 25 | |||
| 26 | # Create and add attribute | ||
| 27 | if i % 5 == 0: | ||
| 28 | # Every 5th item has no attribute (for testing NULL responses) | ||
| 29 | continue | ||
| 30 | |||
| 31 | category = random.choice(["electronics", "furniture", "clothing"]) | ||
| 32 | price = random.randint(50, 1000) | ||
| 33 | attrs = {"category": category, "price": price, "id": i} | ||
| 34 | |||
| 35 | self.redis.execute_command('VSETATTR', self.test_key, name, json.dumps(attrs)) | ||
| 36 | |||
| 37 | def is_numeric(self, value): | ||
| 38 | """Check if a value can be converted to float""" | ||
| 39 | try: | ||
| 40 | if isinstance(value, (int, float)): | ||
| 41 | return True | ||
| 42 | if isinstance(value, bytes): | ||
| 43 | float(value.decode('utf-8')) | ||
| 44 | return True | ||
| 45 | if isinstance(value, str): | ||
| 46 | float(value) | ||
| 47 | return True | ||
| 48 | return False | ||
| 49 | except (ValueError, TypeError): | ||
| 50 | return False | ||
| 51 | |||
| 52 | def test(self): | ||
| 53 | # Create query vector | ||
| 54 | query_vec = generate_random_vector(self.dim) | ||
| 55 | |||
| 56 | # Test 1: VSIM with no additional options (should be same for RESP2 and RESP3) | ||
| 57 | cmd_args = ['VSIM', self.test_key, 'VALUES', self.dim] | ||
| 58 | cmd_args.extend([str(x) for x in query_vec]) | ||
| 59 | cmd_args.extend(['COUNT', 5]) | ||
| 60 | |||
| 61 | results_resp2 = self.redis.execute_command(*cmd_args) | ||
| 62 | results_resp3 = self.redis3.execute_command(*cmd_args) | ||
| 63 | |||
| 64 | # Both should return simple arrays of item names | ||
| 65 | assert len(results_resp2) == 5, f"RESP2: Expected 5 results, got {len(results_resp2)}" | ||
| 66 | assert len(results_resp3) == 5, f"RESP3: Expected 5 results, got {len(results_resp3)}" | ||
| 67 | assert all(isinstance(item, bytes) for item in results_resp2), "RESP2: Results should be byte strings" | ||
| 68 | assert all(isinstance(item, bytes) for item in results_resp3), "RESP3: Results should be byte strings" | ||
| 69 | |||
| 70 | # Test 2: VSIM with WITHSCORES only | ||
| 71 | cmd_args = ['VSIM', self.test_key, 'VALUES', self.dim] | ||
| 72 | cmd_args.extend([str(x) for x in query_vec]) | ||
| 73 | cmd_args.extend(['COUNT', 5, 'WITHSCORES']) | ||
| 74 | |||
| 75 | results_resp2 = self.redis.execute_command(*cmd_args) | ||
| 76 | results_resp3 = self.redis3.execute_command(*cmd_args) | ||
| 77 | |||
| 78 | # RESP2: Should be a flat array alternating item, score | ||
| 79 | assert len(results_resp2) == 10, f"RESP2: Expected 10 elements (5 items × 2), got {len(results_resp2)}" | ||
| 80 | for i in range(0, len(results_resp2), 2): | ||
| 81 | assert isinstance(results_resp2[i], bytes), f"RESP2: Item at {i} should be bytes" | ||
| 82 | assert self.is_numeric(results_resp2[i+1]), f"RESP2: Score at {i+1} should be numeric" | ||
| 83 | score = float(results_resp2[i+1]) if isinstance(results_resp2[i+1], bytes) else results_resp2[i+1] | ||
| 84 | assert 0 <= score <= 1, f"RESP2: Score {score} should be between 0 and 1" | ||
| 85 | |||
| 86 | # RESP3: Should be a dict/map with items as keys and scores as DIRECT values (not arrays) | ||
| 87 | assert isinstance(results_resp3, dict), f"RESP3: Expected dict, got {type(results_resp3)}" | ||
| 88 | assert len(results_resp3) == 5, f"RESP3: Expected 5 entries, got {len(results_resp3)}" | ||
| 89 | for item, score in results_resp3.items(): | ||
| 90 | assert isinstance(item, bytes), f"RESP3: Key should be bytes" | ||
| 91 | # Score should be a direct value, NOT an array | ||
| 92 | assert not isinstance(score, list), f"RESP3: With single WITH option, value should not be array" | ||
| 93 | assert self.is_numeric(score), f"RESP3: Score should be numeric, got {type(score)}" | ||
| 94 | score_val = float(score) if isinstance(score, bytes) else score | ||
| 95 | assert 0 <= score_val <= 1, f"RESP3: Score {score_val} should be between 0 and 1" | ||
| 96 | |||
| 97 | # Test 3: VSIM with WITHATTRIBS only | ||
| 98 | cmd_args = ['VSIM', self.test_key, 'VALUES', self.dim] | ||
| 99 | cmd_args.extend([str(x) for x in query_vec]) | ||
| 100 | cmd_args.extend(['COUNT', 5, 'WITHATTRIBS']) | ||
| 101 | |||
| 102 | results_resp2 = self.redis.execute_command(*cmd_args) | ||
| 103 | results_resp3 = self.redis3.execute_command(*cmd_args) | ||
| 104 | |||
| 105 | # RESP2: Should be a flat array alternating item, attribute | ||
| 106 | assert len(results_resp2) == 10, f"RESP2: Expected 10 elements (5 items × 2), got {len(results_resp2)}" | ||
| 107 | for i in range(0, len(results_resp2), 2): | ||
| 108 | assert isinstance(results_resp2[i], bytes), f"RESP2: Item at {i} should be bytes" | ||
| 109 | attr = results_resp2[i+1] | ||
| 110 | assert attr is None or isinstance(attr, bytes), f"RESP2: Attribute at {i+1} should be None or bytes" | ||
| 111 | if attr is not None: | ||
| 112 | # Verify it's valid JSON | ||
| 113 | json.loads(attr) | ||
| 114 | |||
| 115 | # RESP3: Should be a dict/map with items as keys and attributes as DIRECT values (not arrays) | ||
| 116 | assert isinstance(results_resp3, dict), f"RESP3: Expected dict, got {type(results_resp3)}" | ||
| 117 | assert len(results_resp3) == 5, f"RESP3: Expected 5 entries, got {len(results_resp3)}" | ||
| 118 | for item, attr in results_resp3.items(): | ||
| 119 | assert isinstance(item, bytes), f"RESP3: Key should be bytes" | ||
| 120 | # Attribute should be a direct value, NOT an array | ||
| 121 | assert not isinstance(attr, list), f"RESP3: With single WITH option, value should not be array" | ||
| 122 | assert attr is None or isinstance(attr, bytes), f"RESP3: Attribute should be None or bytes" | ||
| 123 | if attr is not None: | ||
| 124 | # Verify it's valid JSON | ||
| 125 | json.loads(attr) | ||
| 126 | |||
| 127 | # Test 4: VSIM with both WITHSCORES and WITHATTRIBS | ||
| 128 | cmd_args = ['VSIM', self.test_key, 'VALUES', self.dim] | ||
| 129 | cmd_args.extend([str(x) for x in query_vec]) | ||
| 130 | cmd_args.extend(['COUNT', 5, 'WITHSCORES', 'WITHATTRIBS']) | ||
| 131 | |||
| 132 | results_resp2 = self.redis.execute_command(*cmd_args) | ||
| 133 | results_resp3 = self.redis3.execute_command(*cmd_args) | ||
| 134 | |||
| 135 | # RESP2: Should be a flat array with pattern: item, score, attribute | ||
| 136 | assert len(results_resp2) == 15, f"RESP2: Expected 15 elements (5 items × 3), got {len(results_resp2)}" | ||
| 137 | for i in range(0, len(results_resp2), 3): | ||
| 138 | assert isinstance(results_resp2[i], bytes), f"RESP2: Item at {i} should be bytes" | ||
| 139 | assert self.is_numeric(results_resp2[i+1]), f"RESP2: Score at {i+1} should be numeric" | ||
| 140 | score = float(results_resp2[i+1]) if isinstance(results_resp2[i+1], bytes) else results_resp2[i+1] | ||
| 141 | assert 0 <= score <= 1, f"RESP2: Score {score} should be between 0 and 1" | ||
| 142 | attr = results_resp2[i+2] | ||
| 143 | assert attr is None or isinstance(attr, bytes), f"RESP2: Attribute at {i+2} should be None or bytes" | ||
| 144 | |||
| 145 | # RESP3: Should be a dict where each value is a 2-element array [score, attribute] | ||
| 146 | assert isinstance(results_resp3, dict), f"RESP3: Expected dict, got {type(results_resp3)}" | ||
| 147 | assert len(results_resp3) == 5, f"RESP3: Expected 5 entries, got {len(results_resp3)}" | ||
| 148 | for item, value in results_resp3.items(): | ||
| 149 | assert isinstance(item, bytes), f"RESP3: Key should be bytes" | ||
| 150 | # With BOTH options, value MUST be an array | ||
| 151 | assert isinstance(value, list), f"RESP3: With both WITH options, value should be a list, got {type(value)}" | ||
| 152 | assert len(value) == 2, f"RESP3: Value should have 2 elements [score, attr], got {len(value)}" | ||
| 153 | |||
| 154 | score, attr = value | ||
| 155 | assert self.is_numeric(score), f"RESP3: Score should be numeric" | ||
| 156 | score_val = float(score) if isinstance(score, bytes) else score | ||
| 157 | assert 0 <= score_val <= 1, f"RESP3: Score {score_val} should be between 0 and 1" | ||
| 158 | assert attr is None or isinstance(attr, bytes), f"RESP3: Attribute should be None or bytes" | ||
| 159 | |||
| 160 | # Test 5: Verify consistency - same items returned in same order | ||
| 161 | cmd_args = ['VSIM', self.test_key, 'VALUES', self.dim] | ||
| 162 | cmd_args.extend([str(x) for x in query_vec]) | ||
| 163 | cmd_args.extend(['COUNT', 5, 'WITHSCORES', 'WITHATTRIBS']) | ||
| 164 | |||
| 165 | results_resp2 = self.redis.execute_command(*cmd_args) | ||
| 166 | results_resp3 = self.redis3.execute_command(*cmd_args) | ||
| 167 | |||
| 168 | # Extract items from RESP2 (every 3rd element starting from 0) | ||
| 169 | items_resp2 = [results_resp2[i] for i in range(0, len(results_resp2), 3)] | ||
| 170 | |||
| 171 | # Extract items from RESP3 (keys of the dict) | ||
| 172 | items_resp3 = list(results_resp3.keys()) | ||
| 173 | |||
| 174 | # Verify same items returned | ||
| 175 | assert set(items_resp2) == set(items_resp3), "RESP2 and RESP3 should return the same items" | ||
| 176 | |||
| 177 | # Build a mapping from items to scores and attributes for comparison | ||
| 178 | data_resp2 = {} | ||
| 179 | for i in range(0, len(results_resp2), 3): | ||
| 180 | item = results_resp2[i] | ||
| 181 | score = float(results_resp2[i+1]) if isinstance(results_resp2[i+1], bytes) else results_resp2[i+1] | ||
| 182 | attr = results_resp2[i+2] | ||
| 183 | data_resp2[item] = (score, attr) | ||
| 184 | |||
| 185 | data_resp3 = {} | ||
| 186 | for item, value in results_resp3.items(): | ||
| 187 | score = float(value[0]) if isinstance(value[0], bytes) else value[0] | ||
| 188 | attr = value[1] | ||
| 189 | data_resp3[item] = (score, attr) | ||
| 190 | |||
| 191 | # Verify scores and attributes match for each item | ||
| 192 | for item in data_resp2: | ||
| 193 | score_resp2, attr_resp2 = data_resp2[item] | ||
| 194 | score_resp3, attr_resp3 = data_resp3[item] | ||
| 195 | |||
| 196 | assert abs(score_resp2 - score_resp3) < 0.0001, \ | ||
| 197 | f"Scores for {item} don't match: RESP2={score_resp2}, RESP3={score_resp3}" | ||
| 198 | assert attr_resp2 == attr_resp3, \ | ||
| 199 | f"Attributes for {item} don't match: RESP2={attr_resp2}, RESP3={attr_resp3}" | ||
| 200 | |||
| 201 | # Test 6: Test ordering of WITHSCORES and WITHATTRIBS doesn't matter | ||
| 202 | cmd_args1 = ['VSIM', self.test_key, 'VALUES', self.dim] | ||
| 203 | cmd_args1.extend([str(x) for x in query_vec]) | ||
| 204 | cmd_args1.extend(['COUNT', 3, 'WITHSCORES', 'WITHATTRIBS']) | ||
| 205 | |||
| 206 | cmd_args2 = ['VSIM', self.test_key, 'VALUES', self.dim] | ||
| 207 | cmd_args2.extend([str(x) for x in query_vec]) | ||
| 208 | cmd_args2.extend(['COUNT', 3, 'WITHATTRIBS', 'WITHSCORES']) # Reversed order | ||
| 209 | |||
| 210 | results1_resp3 = self.redis3.execute_command(*cmd_args1) | ||
| 211 | results2_resp3 = self.redis3.execute_command(*cmd_args2) | ||
| 212 | |||
| 213 | # Both should return the same structure | ||
| 214 | assert results1_resp3 == results2_resp3, "Order of WITH options shouldn't matter" | ||
