Skip to content

Commit c5b4f47

Browse files
authored
feat: add support for vector sets (#2998)
* wip * improve the vadd api * resp3 tests * fix some tests * extract json helper functions in client package * use transformJsonReply * remove the CACHEABLE flag for all vector set commands currently, client side caching is not supported for vector set commands by the server * properly transform vinfo result * add resp3 test for vlinks * add more tests for vrandmember * fix vrem return types * fix vsetattr return type * fix vsim_withscores * implement vlinks_withscores * set minimum docker image version to 8 * align return types * add RAW variant for VEMB -> VEMB_RAW * use the new parseCommand api
1 parent b521777 commit c5b4f47

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1608
-34
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { strict as assert } from 'node:assert';
2+
import testUtils, { GLOBAL } from '../test-utils';
3+
import VADD from './VADD';
4+
import { BasicCommandParser } from '../client/parser';
5+
6+
describe('VADD', () => {
7+
describe('parseCommand', () => {
8+
it('basic usage', () => {
9+
const parser = new BasicCommandParser();
10+
VADD.parseCommand(parser, 'key', [1.0, 2.0, 3.0], 'element');
11+
assert.deepEqual(
12+
parser.redisArgs,
13+
['VADD', 'key', 'VALUES', '3', '1', '2', '3', 'element']
14+
);
15+
});
16+
17+
it('with REDUCE option', () => {
18+
const parser = new BasicCommandParser();
19+
VADD.parseCommand(parser, 'key', [1.0, 2], 'element', { REDUCE: 50 });
20+
assert.deepEqual(
21+
parser.redisArgs,
22+
['VADD', 'key', 'REDUCE', '50', 'VALUES', '2', '1', '2', 'element']
23+
);
24+
});
25+
26+
it('with quantization options', () => {
27+
let parser = new BasicCommandParser();
28+
VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'Q8' });
29+
assert.deepEqual(
30+
parser.redisArgs,
31+
['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'Q8']
32+
);
33+
34+
parser = new BasicCommandParser();
35+
VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'BIN' });
36+
assert.deepEqual(
37+
parser.redisArgs,
38+
['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'BIN']
39+
);
40+
41+
parser = new BasicCommandParser();
42+
VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'NOQUANT' });
43+
assert.deepEqual(
44+
parser.redisArgs,
45+
['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'NOQUANT']
46+
);
47+
});
48+
49+
it('with all options', () => {
50+
const parser = new BasicCommandParser();
51+
VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', {
52+
REDUCE: 50,
53+
CAS: true,
54+
QUANT: 'Q8',
55+
EF: 200,
56+
SETATTR: { name: 'test', value: 42 },
57+
M: 16
58+
});
59+
assert.deepEqual(
60+
parser.redisArgs,
61+
[
62+
'VADD', 'key', 'REDUCE', '50', 'VALUES', '2', '1', '2', 'element',
63+
'CAS', 'Q8', 'EF', '200', 'SETATTR', '{"name":"test","value":42}', 'M', '16'
64+
]
65+
);
66+
});
67+
});
68+
69+
testUtils.testAll('vAdd', async client => {
70+
assert.equal(
71+
await client.vAdd('key', [1.0, 2.0, 3.0], 'element'),
72+
true
73+
);
74+
75+
// same element should not be added again
76+
assert.equal(
77+
await client.vAdd('key', [1, 2 , 3], 'element'),
78+
false
79+
);
80+
81+
}, {
82+
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
83+
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] },
84+
});
85+
86+
testUtils.testWithClient('vAdd with RESP3', async client => {
87+
// Test basic functionality with RESP3
88+
assert.equal(
89+
await client.vAdd('resp3-key', [1.5, 2.5, 3.5], 'resp3-element'),
90+
true
91+
);
92+
93+
// same element should not be added again
94+
assert.equal(
95+
await client.vAdd('resp3-key', [1, 2 , 3], 'resp3-element'),
96+
false
97+
);
98+
99+
// Test with options to ensure complex parameters work with RESP3
100+
assert.equal(
101+
await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'resp3-element2', {
102+
QUANT: 'Q8',
103+
CAS: true,
104+
SETATTR: { type: 'test', value: 123 }
105+
}),
106+
true
107+
);
108+
109+
// Verify the vector set was created correctly
110+
assert.equal(
111+
await client.vCard('resp3-key'),
112+
2
113+
);
114+
}, {
115+
...GLOBAL.SERVERS.OPEN,
116+
clientOptions: {
117+
RESP: 3
118+
},
119+
minimumDockerVersion: [8, 0]
120+
});
121+
});

packages/client/lib/commands/VADD.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { CommandParser } from '../client/parser';
2+
import { RedisArgument, Command } from '../RESP/types';
3+
import { transformBooleanReply, transformDoubleArgument } from './generic-transformers';
4+
5+
export interface VAddOptions {
6+
REDUCE?: number;
7+
CAS?: boolean;
8+
QUANT?: 'NOQUANT' | 'BIN' | 'Q8',
9+
EF?: number;
10+
SETATTR?: Record<string, any>;
11+
M?: number;
12+
}
13+
14+
export default {
15+
/**
16+
* Add a new element into the vector set specified by key
17+
*
18+
* @param parser - The command parser
19+
* @param key - The name of the key that will hold the vector set data
20+
* @param vector - The vector data as array of numbers
21+
* @param element - The name of the element being added to the vector set
22+
* @param options - Optional parameters for vector addition
23+
* @see https://redis.io/commands/vadd/
24+
*/
25+
parseCommand(
26+
parser: CommandParser,
27+
key: RedisArgument,
28+
vector: Array<number>,
29+
element: RedisArgument,
30+
options?: VAddOptions
31+
) {
32+
parser.push('VADD');
33+
parser.pushKey(key);
34+
35+
if (options?.REDUCE !== undefined) {
36+
parser.push('REDUCE', options.REDUCE.toString());
37+
}
38+
39+
parser.push('VALUES', vector.length.toString());
40+
for (const value of vector) {
41+
parser.push(transformDoubleArgument(value));
42+
}
43+
44+
parser.push(element);
45+
46+
if (options?.CAS) {
47+
parser.push('CAS');
48+
}
49+
50+
options?.QUANT && parser.push(options.QUANT);
51+
52+
if (options?.EF !== undefined) {
53+
parser.push('EF', options.EF.toString());
54+
}
55+
56+
if (options?.SETATTR) {
57+
parser.push('SETATTR', JSON.stringify(options.SETATTR));
58+
}
59+
60+
if (options?.M !== undefined) {
61+
parser.push('M', options.M.toString());
62+
}
63+
},
64+
transformReply: transformBooleanReply
65+
} as const satisfies Command;
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { strict as assert } from 'node:assert';
2+
import testUtils, { GLOBAL } from '../test-utils';
3+
import VCARD from './VCARD';
4+
import { BasicCommandParser } from '../client/parser';
5+
6+
describe('VCARD', () => {
7+
it('parseCommand', () => {
8+
const parser = new BasicCommandParser();
9+
VCARD.parseCommand(parser, 'key')
10+
assert.deepEqual(
11+
parser.redisArgs,
12+
['VCARD', 'key']
13+
);
14+
});
15+
16+
testUtils.testAll('vCard', async client => {
17+
await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
18+
await client.vAdd('key', [4.0, 5.0, 6.0], 'element2');
19+
20+
assert.equal(
21+
await client.vCard('key'),
22+
2
23+
);
24+
25+
assert.equal(await client.vCard('unknown'), 0);
26+
}, {
27+
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
28+
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
29+
});
30+
31+
testUtils.testWithClient('vCard with RESP3', async client => {
32+
// Test empty vector set
33+
assert.equal(
34+
await client.vCard('resp3-empty-key'),
35+
0
36+
);
37+
38+
// Add elements and test cardinality
39+
await client.vAdd('resp3-key', [1.0, 2.0], 'elem1');
40+
assert.equal(
41+
await client.vCard('resp3-key'),
42+
1
43+
);
44+
45+
await client.vAdd('resp3-key', [3.0, 4.0], 'elem2');
46+
await client.vAdd('resp3-key', [5.0, 6.0], 'elem3');
47+
assert.equal(
48+
await client.vCard('resp3-key'),
49+
3
50+
);
51+
52+
assert.equal(await client.vCard('unknown'), 0);
53+
}, {
54+
...GLOBAL.SERVERS.OPEN,
55+
clientOptions: {
56+
RESP: 3
57+
},
58+
minimumDockerVersion: [8, 0]
59+
});
60+
});

packages/client/lib/commands/VCARD.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { CommandParser } from '../client/parser';
2+
import { RedisArgument, NumberReply, Command } from '../RESP/types';
3+
4+
export default {
5+
IS_READ_ONLY: true,
6+
/**
7+
* Retrieve the number of elements in a vector set
8+
*
9+
* @param parser - The command parser
10+
* @param key - The key of the vector set
11+
* @see https://redis.io/commands/vcard/
12+
*/
13+
parseCommand(parser: CommandParser, key: RedisArgument) {
14+
parser.push('VCARD');
15+
parser.pushKey(key);
16+
},
17+
transformReply: undefined as unknown as () => NumberReply
18+
} as const satisfies Command;
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { strict as assert } from 'node:assert';
2+
import testUtils, { GLOBAL } from '../test-utils';
3+
import VDIM from './VDIM';
4+
import { BasicCommandParser } from '../client/parser';
5+
6+
describe('VDIM', () => {
7+
it('parseCommand', () => {
8+
const parser = new BasicCommandParser();
9+
VDIM.parseCommand(parser, 'key');
10+
assert.deepEqual(
11+
parser.redisArgs,
12+
['VDIM', 'key']
13+
);
14+
});
15+
16+
testUtils.testAll('vDim', async client => {
17+
await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
18+
19+
assert.equal(
20+
await client.vDim('key'),
21+
3
22+
);
23+
}, {
24+
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
25+
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
26+
});
27+
28+
testUtils.testWithClient('vDim with RESP3', async client => {
29+
await client.vAdd('resp3-5d', [1.0, 2.0, 3.0, 4.0, 5.0], 'elem5d');
30+
31+
assert.equal(
32+
await client.vDim('resp3-5d'),
33+
5
34+
);
35+
36+
}, {
37+
...GLOBAL.SERVERS.OPEN,
38+
clientOptions: {
39+
RESP: 3
40+
},
41+
minimumDockerVersion: [8, 0]
42+
});
43+
});

packages/client/lib/commands/VDIM.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { CommandParser } from '../client/parser';
2+
import { RedisArgument, NumberReply, Command } from '../RESP/types';
3+
4+
export default {
5+
IS_READ_ONLY: true,
6+
/**
7+
* Retrieve the dimension of the vectors in a vector set
8+
*
9+
* @param parser - The command parser
10+
* @param key - The key of the vector set
11+
* @see https://redis.io/commands/vdim/
12+
*/
13+
parseCommand(parser: CommandParser, key: RedisArgument) {
14+
parser.push('VDIM');
15+
parser.pushKey(key);
16+
},
17+
transformReply: undefined as unknown as () => NumberReply
18+
} as const satisfies Command;
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { strict as assert } from 'node:assert';
2+
import testUtils, { GLOBAL } from '../test-utils';
3+
import VEMB from './VEMB';
4+
import { BasicCommandParser } from '../client/parser';
5+
6+
describe('VEMB', () => {
7+
it('parseCommand', () => {
8+
const parser = new BasicCommandParser();
9+
VEMB.parseCommand(parser, 'key', 'element');
10+
assert.deepEqual(
11+
parser.redisArgs,
12+
['VEMB', 'key', 'element']
13+
);
14+
});
15+
16+
testUtils.testAll('vEmb', async client => {
17+
await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
18+
19+
const result = await client.vEmb('key', 'element');
20+
assert.ok(Array.isArray(result));
21+
assert.equal(result.length, 3);
22+
assert.equal(typeof result[0], 'number');
23+
}, {
24+
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
25+
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
26+
});
27+
28+
testUtils.testWithClient('vEmb with RESP3', async client => {
29+
await client.vAdd('resp3-key', [1.5, 2.5, 3.5, 4.5], 'resp3-element');
30+
31+
const result = await client.vEmb('resp3-key', 'resp3-element');
32+
assert.ok(Array.isArray(result));
33+
assert.equal(result.length, 4);
34+
assert.equal(typeof result[0], 'number');
35+
}, {
36+
...GLOBAL.SERVERS.OPEN,
37+
clientOptions: {
38+
RESP: 3
39+
},
40+
minimumDockerVersion: [8, 0]
41+
});
42+
});

packages/client/lib/commands/VEMB.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { CommandParser } from '../client/parser';
2+
import { RedisArgument, Command } from '../RESP/types';
3+
import { transformDoubleArrayReply } from './generic-transformers';
4+
5+
export default {
6+
IS_READ_ONLY: true,
7+
/**
8+
* Retrieve the approximate vector associated with a vector set element
9+
*
10+
* @param parser - The command parser
11+
* @param key - The key of the vector set
12+
* @param element - The name of the element to retrieve the vector for
13+
* @see https://redis.io/commands/vemb/
14+
*/
15+
parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) {
16+
parser.push('VEMB');
17+
parser.pushKey(key);
18+
parser.push(element);
19+
},
20+
transformReply: transformDoubleArrayReply
21+
} as const satisfies Command;

0 commit comments

Comments
 (0)