File: with.py

package info (click to toggle)
redis 5%3A8.0.4-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 21,916 kB
  • sloc: ansic: 217,142; tcl: 51,874; sh: 4,625; perl: 4,214; cpp: 3,568; python: 3,165; makefile: 2,055; ruby: 639; javascript: 30; csh: 7
file content (214 lines) | stat: -rw-r--r-- 10,946 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
from test import TestCase, generate_random_vector
import struct
import json
import random

class VSIMWithAttribs(TestCase):
    def getname(self):
        return "VSIM WITHATTRIBS/WITHSCORES functionality testing"

    def setup(self):
        super().setup()
        self.dim = 8
        self.count = 20

        # Create vectors with attributes
        for i in range(self.count):
            vec = generate_random_vector(self.dim)
            vec_bytes = struct.pack(f'{self.dim}f', *vec)

            # Item name
            name = f"{self.test_key}:item:{i}"

            # Add to Redis
            self.redis.execute_command('VADD', self.test_key, 'FP32', vec_bytes, name)

            # Create and add attribute
            if i % 5 == 0:
                # Every 5th item has no attribute (for testing NULL responses)
                continue

            category = random.choice(["electronics", "furniture", "clothing"])
            price = random.randint(50, 1000)
            attrs = {"category": category, "price": price, "id": i}

            self.redis.execute_command('VSETATTR', self.test_key, name, json.dumps(attrs))

    def is_numeric(self, value):
        """Check if a value can be converted to float"""
        try:
            if isinstance(value, (int, float)):
                return True
            if isinstance(value, bytes):
                float(value.decode('utf-8'))
                return True
            if isinstance(value, str):
                float(value)
                return True
            return False
        except (ValueError, TypeError):
            return False

    def test(self):
        # Create query vector
        query_vec = generate_random_vector(self.dim)

        # Test 1: VSIM with no additional options (should be same for RESP2 and RESP3)
        cmd_args = ['VSIM', self.test_key, 'VALUES', self.dim]
        cmd_args.extend([str(x) for x in query_vec])
        cmd_args.extend(['COUNT', 5])

        results_resp2 = self.redis.execute_command(*cmd_args)
        results_resp3 = self.redis3.execute_command(*cmd_args)

        # Both should return simple arrays of item names
        assert len(results_resp2) == 5, f"RESP2: Expected 5 results, got {len(results_resp2)}"
        assert len(results_resp3) == 5, f"RESP3: Expected 5 results, got {len(results_resp3)}"
        assert all(isinstance(item, bytes) for item in results_resp2), "RESP2: Results should be byte strings"
        assert all(isinstance(item, bytes) for item in results_resp3), "RESP3: Results should be byte strings"

        # Test 2: VSIM with WITHSCORES only
        cmd_args = ['VSIM', self.test_key, 'VALUES', self.dim]
        cmd_args.extend([str(x) for x in query_vec])
        cmd_args.extend(['COUNT', 5, 'WITHSCORES'])

        results_resp2 = self.redis.execute_command(*cmd_args)
        results_resp3 = self.redis3.execute_command(*cmd_args)

        # RESP2: Should be a flat array alternating item, score
        assert len(results_resp2) == 10, f"RESP2: Expected 10 elements (5 items × 2), got {len(results_resp2)}"
        for i in range(0, len(results_resp2), 2):
            assert isinstance(results_resp2[i], bytes), f"RESP2: Item at {i} should be bytes"
            assert self.is_numeric(results_resp2[i+1]), f"RESP2: Score at {i+1} should be numeric"
            score = float(results_resp2[i+1]) if isinstance(results_resp2[i+1], bytes) else results_resp2[i+1]
            assert 0 <= score <= 1, f"RESP2: Score {score} should be between 0 and 1"

        # RESP3: Should be a dict/map with items as keys and scores as DIRECT values (not arrays)
        assert isinstance(results_resp3, dict), f"RESP3: Expected dict, got {type(results_resp3)}"
        assert len(results_resp3) == 5, f"RESP3: Expected 5 entries, got {len(results_resp3)}"
        for item, score in results_resp3.items():
            assert isinstance(item, bytes), f"RESP3: Key should be bytes"
            # Score should be a direct value, NOT an array
            assert not isinstance(score, list), f"RESP3: With single WITH option, value should not be array"
            assert self.is_numeric(score), f"RESP3: Score should be numeric, got {type(score)}"
            score_val = float(score) if isinstance(score, bytes) else score
            assert 0 <= score_val <= 1, f"RESP3: Score {score_val} should be between 0 and 1"

        # Test 3: VSIM with WITHATTRIBS only
        cmd_args = ['VSIM', self.test_key, 'VALUES', self.dim]
        cmd_args.extend([str(x) for x in query_vec])
        cmd_args.extend(['COUNT', 5, 'WITHATTRIBS'])

        results_resp2 = self.redis.execute_command(*cmd_args)
        results_resp3 = self.redis3.execute_command(*cmd_args)

        # RESP2: Should be a flat array alternating item, attribute
        assert len(results_resp2) == 10, f"RESP2: Expected 10 elements (5 items × 2), got {len(results_resp2)}"
        for i in range(0, len(results_resp2), 2):
            assert isinstance(results_resp2[i], bytes), f"RESP2: Item at {i} should be bytes"
            attr = results_resp2[i+1]
            assert attr is None or isinstance(attr, bytes), f"RESP2: Attribute at {i+1} should be None or bytes"
            if attr is not None:
                # Verify it's valid JSON
                json.loads(attr)

        # RESP3: Should be a dict/map with items as keys and attributes as DIRECT values (not arrays)
        assert isinstance(results_resp3, dict), f"RESP3: Expected dict, got {type(results_resp3)}"
        assert len(results_resp3) == 5, f"RESP3: Expected 5 entries, got {len(results_resp3)}"
        for item, attr in results_resp3.items():
            assert isinstance(item, bytes), f"RESP3: Key should be bytes"
            # Attribute should be a direct value, NOT an array
            assert not isinstance(attr, list), f"RESP3: With single WITH option, value should not be array"
            assert attr is None or isinstance(attr, bytes), f"RESP3: Attribute should be None or bytes"
            if attr is not None:
                # Verify it's valid JSON
                json.loads(attr)

        # Test 4: VSIM with both WITHSCORES and WITHATTRIBS
        cmd_args = ['VSIM', self.test_key, 'VALUES', self.dim]
        cmd_args.extend([str(x) for x in query_vec])
        cmd_args.extend(['COUNT', 5, 'WITHSCORES', 'WITHATTRIBS'])

        results_resp2 = self.redis.execute_command(*cmd_args)
        results_resp3 = self.redis3.execute_command(*cmd_args)

        # RESP2: Should be a flat array with pattern: item, score, attribute
        assert len(results_resp2) == 15, f"RESP2: Expected 15 elements (5 items × 3), got {len(results_resp2)}"
        for i in range(0, len(results_resp2), 3):
            assert isinstance(results_resp2[i], bytes), f"RESP2: Item at {i} should be bytes"
            assert self.is_numeric(results_resp2[i+1]), f"RESP2: Score at {i+1} should be numeric"
            score = float(results_resp2[i+1]) if isinstance(results_resp2[i+1], bytes) else results_resp2[i+1]
            assert 0 <= score <= 1, f"RESP2: Score {score} should be between 0 and 1"
            attr = results_resp2[i+2]
            assert attr is None or isinstance(attr, bytes), f"RESP2: Attribute at {i+2} should be None or bytes"

        # RESP3: Should be a dict where each value is a 2-element array [score, attribute]
        assert isinstance(results_resp3, dict), f"RESP3: Expected dict, got {type(results_resp3)}"
        assert len(results_resp3) == 5, f"RESP3: Expected 5 entries, got {len(results_resp3)}"
        for item, value in results_resp3.items():
            assert isinstance(item, bytes), f"RESP3: Key should be bytes"
            # With BOTH options, value MUST be an array
            assert isinstance(value, list), f"RESP3: With both WITH options, value should be a list, got {type(value)}"
            assert len(value) == 2, f"RESP3: Value should have 2 elements [score, attr], got {len(value)}"

            score, attr = value
            assert self.is_numeric(score), f"RESP3: Score should be numeric"
            score_val = float(score) if isinstance(score, bytes) else score
            assert 0 <= score_val <= 1, f"RESP3: Score {score_val} should be between 0 and 1"
            assert attr is None or isinstance(attr, bytes), f"RESP3: Attribute should be None or bytes"

        # Test 5: Verify consistency - same items returned in same order
        cmd_args = ['VSIM', self.test_key, 'VALUES', self.dim]
        cmd_args.extend([str(x) for x in query_vec])
        cmd_args.extend(['COUNT', 5, 'WITHSCORES', 'WITHATTRIBS'])

        results_resp2 = self.redis.execute_command(*cmd_args)
        results_resp3 = self.redis3.execute_command(*cmd_args)

        # Extract items from RESP2 (every 3rd element starting from 0)
        items_resp2 = [results_resp2[i] for i in range(0, len(results_resp2), 3)]

        # Extract items from RESP3 (keys of the dict)
        items_resp3 = list(results_resp3.keys())

        # Verify same items returned
        assert set(items_resp2) == set(items_resp3), "RESP2 and RESP3 should return the same items"

        # Build a mapping from items to scores and attributes for comparison
        data_resp2 = {}
        for i in range(0, len(results_resp2), 3):
            item = results_resp2[i]
            score = float(results_resp2[i+1]) if isinstance(results_resp2[i+1], bytes) else results_resp2[i+1]
            attr = results_resp2[i+2]
            data_resp2[item] = (score, attr)

        data_resp3 = {}
        for item, value in results_resp3.items():
            score = float(value[0]) if isinstance(value[0], bytes) else value[0]
            attr = value[1]
            data_resp3[item] = (score, attr)

        # Verify scores and attributes match for each item
        for item in data_resp2:
            score_resp2, attr_resp2 = data_resp2[item]
            score_resp3, attr_resp3 = data_resp3[item]

            assert abs(score_resp2 - score_resp3) < 0.0001, \
                f"Scores for {item} don't match: RESP2={score_resp2}, RESP3={score_resp3}"
            assert attr_resp2 == attr_resp3, \
                f"Attributes for {item} don't match: RESP2={attr_resp2}, RESP3={attr_resp3}"

        # Test 6: Test ordering of WITHSCORES and WITHATTRIBS doesn't matter
        cmd_args1 = ['VSIM', self.test_key, 'VALUES', self.dim]
        cmd_args1.extend([str(x) for x in query_vec])
        cmd_args1.extend(['COUNT', 3, 'WITHSCORES', 'WITHATTRIBS'])

        cmd_args2 = ['VSIM', self.test_key, 'VALUES', self.dim]
        cmd_args2.extend([str(x) for x in query_vec])
        cmd_args2.extend(['COUNT', 3, 'WITHATTRIBS', 'WITHSCORES'])  # Reversed order

        results1_resp3 = self.redis3.execute_command(*cmd_args1)
        results2_resp3 = self.redis3.execute_command(*cmd_args2)

        # Both should return the same structure
        assert results1_resp3 == results2_resp3, "Order of WITH options shouldn't matter"