You've already forked node-redis
mirror of
https://github.com/redis/node-redis.git
synced 2025-08-06 02:15:48 +03:00
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
This commit is contained in:
committed by
GitHub
parent
b52177752e
commit
c5b4f47975
121
packages/client/lib/commands/VADD.spec.ts
Normal file
121
packages/client/lib/commands/VADD.spec.ts
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VADD from './VADD';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VADD', () => {
|
||||||
|
describe('parseCommand', () => {
|
||||||
|
it('basic usage', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VADD.parseCommand(parser, 'key', [1.0, 2.0, 3.0], 'element');
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VADD', 'key', 'VALUES', '3', '1', '2', '3', 'element']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with REDUCE option', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VADD.parseCommand(parser, 'key', [1.0, 2], 'element', { REDUCE: 50 });
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VADD', 'key', 'REDUCE', '50', 'VALUES', '2', '1', '2', 'element']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with quantization options', () => {
|
||||||
|
let parser = new BasicCommandParser();
|
||||||
|
VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'Q8' });
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'Q8']
|
||||||
|
);
|
||||||
|
|
||||||
|
parser = new BasicCommandParser();
|
||||||
|
VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'BIN' });
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'BIN']
|
||||||
|
);
|
||||||
|
|
||||||
|
parser = new BasicCommandParser();
|
||||||
|
VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', { QUANT: 'NOQUANT' });
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VADD', 'key', 'VALUES', '2', '1', '2', 'element', 'NOQUANT']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with all options', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VADD.parseCommand(parser, 'key', [1.0, 2.0], 'element', {
|
||||||
|
REDUCE: 50,
|
||||||
|
CAS: true,
|
||||||
|
QUANT: 'Q8',
|
||||||
|
EF: 200,
|
||||||
|
SETATTR: { name: 'test', value: 42 },
|
||||||
|
M: 16
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
[
|
||||||
|
'VADD', 'key', 'REDUCE', '50', 'VALUES', '2', '1', '2', 'element',
|
||||||
|
'CAS', 'Q8', 'EF', '200', 'SETATTR', '{"name":"test","value":42}', 'M', '16'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vAdd', async client => {
|
||||||
|
assert.equal(
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// same element should not be added again
|
||||||
|
assert.equal(
|
||||||
|
await client.vAdd('key', [1, 2 , 3], 'element'),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vAdd with RESP3', async client => {
|
||||||
|
// Test basic functionality with RESP3
|
||||||
|
assert.equal(
|
||||||
|
await client.vAdd('resp3-key', [1.5, 2.5, 3.5], 'resp3-element'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// same element should not be added again
|
||||||
|
assert.equal(
|
||||||
|
await client.vAdd('resp3-key', [1, 2 , 3], 'resp3-element'),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Test with options to ensure complex parameters work with RESP3
|
||||||
|
assert.equal(
|
||||||
|
await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'resp3-element2', {
|
||||||
|
QUANT: 'Q8',
|
||||||
|
CAS: true,
|
||||||
|
SETATTR: { type: 'test', value: 123 }
|
||||||
|
}),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
// Verify the vector set was created correctly
|
||||||
|
assert.equal(
|
||||||
|
await client.vCard('resp3-key'),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
65
packages/client/lib/commands/VADD.ts
Normal file
65
packages/client/lib/commands/VADD.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import { RedisArgument, Command } from '../RESP/types';
|
||||||
|
import { transformBooleanReply, transformDoubleArgument } from './generic-transformers';
|
||||||
|
|
||||||
|
export interface VAddOptions {
|
||||||
|
REDUCE?: number;
|
||||||
|
CAS?: boolean;
|
||||||
|
QUANT?: 'NOQUANT' | 'BIN' | 'Q8',
|
||||||
|
EF?: number;
|
||||||
|
SETATTR?: Record<string, any>;
|
||||||
|
M?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Add a new element into the vector set specified by key
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The name of the key that will hold the vector set data
|
||||||
|
* @param vector - The vector data as array of numbers
|
||||||
|
* @param element - The name of the element being added to the vector set
|
||||||
|
* @param options - Optional parameters for vector addition
|
||||||
|
* @see https://redis.io/commands/vadd/
|
||||||
|
*/
|
||||||
|
parseCommand(
|
||||||
|
parser: CommandParser,
|
||||||
|
key: RedisArgument,
|
||||||
|
vector: Array<number>,
|
||||||
|
element: RedisArgument,
|
||||||
|
options?: VAddOptions
|
||||||
|
) {
|
||||||
|
parser.push('VADD');
|
||||||
|
parser.pushKey(key);
|
||||||
|
|
||||||
|
if (options?.REDUCE !== undefined) {
|
||||||
|
parser.push('REDUCE', options.REDUCE.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.push('VALUES', vector.length.toString());
|
||||||
|
for (const value of vector) {
|
||||||
|
parser.push(transformDoubleArgument(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
parser.push(element);
|
||||||
|
|
||||||
|
if (options?.CAS) {
|
||||||
|
parser.push('CAS');
|
||||||
|
}
|
||||||
|
|
||||||
|
options?.QUANT && parser.push(options.QUANT);
|
||||||
|
|
||||||
|
if (options?.EF !== undefined) {
|
||||||
|
parser.push('EF', options.EF.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.SETATTR) {
|
||||||
|
parser.push('SETATTR', JSON.stringify(options.SETATTR));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.M !== undefined) {
|
||||||
|
parser.push('M', options.M.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transformReply: transformBooleanReply
|
||||||
|
} as const satisfies Command;
|
60
packages/client/lib/commands/VCARD.spec.ts
Normal file
60
packages/client/lib/commands/VCARD.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VCARD from './VCARD';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VCARD', () => {
|
||||||
|
it('parseCommand', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VCARD.parseCommand(parser, 'key')
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VCARD', 'key']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vCard', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('key', [4.0, 5.0, 6.0], 'element2');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.vCard('key'),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(await client.vCard('unknown'), 0);
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vCard with RESP3', async client => {
|
||||||
|
// Test empty vector set
|
||||||
|
assert.equal(
|
||||||
|
await client.vCard('resp3-empty-key'),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add elements and test cardinality
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0], 'elem1');
|
||||||
|
assert.equal(
|
||||||
|
await client.vCard('resp3-key'),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
await client.vAdd('resp3-key', [3.0, 4.0], 'elem2');
|
||||||
|
await client.vAdd('resp3-key', [5.0, 6.0], 'elem3');
|
||||||
|
assert.equal(
|
||||||
|
await client.vCard('resp3-key'),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(await client.vCard('unknown'), 0);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
18
packages/client/lib/commands/VCARD.ts
Normal file
18
packages/client/lib/commands/VCARD.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import { RedisArgument, NumberReply, Command } from '../RESP/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
/**
|
||||||
|
* Retrieve the number of elements in a vector set
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The key of the vector set
|
||||||
|
* @see https://redis.io/commands/vcard/
|
||||||
|
*/
|
||||||
|
parseCommand(parser: CommandParser, key: RedisArgument) {
|
||||||
|
parser.push('VCARD');
|
||||||
|
parser.pushKey(key);
|
||||||
|
},
|
||||||
|
transformReply: undefined as unknown as () => NumberReply
|
||||||
|
} as const satisfies Command;
|
43
packages/client/lib/commands/VDIM.spec.ts
Normal file
43
packages/client/lib/commands/VDIM.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VDIM from './VDIM';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VDIM', () => {
|
||||||
|
it('parseCommand', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VDIM.parseCommand(parser, 'key');
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VDIM', 'key']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vDim', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.vDim('key'),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vDim with RESP3', async client => {
|
||||||
|
await client.vAdd('resp3-5d', [1.0, 2.0, 3.0, 4.0, 5.0], 'elem5d');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.vDim('resp3-5d'),
|
||||||
|
5
|
||||||
|
);
|
||||||
|
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
18
packages/client/lib/commands/VDIM.ts
Normal file
18
packages/client/lib/commands/VDIM.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import { RedisArgument, NumberReply, Command } from '../RESP/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
/**
|
||||||
|
* Retrieve the dimension of the vectors in a vector set
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The key of the vector set
|
||||||
|
* @see https://redis.io/commands/vdim/
|
||||||
|
*/
|
||||||
|
parseCommand(parser: CommandParser, key: RedisArgument) {
|
||||||
|
parser.push('VDIM');
|
||||||
|
parser.pushKey(key);
|
||||||
|
},
|
||||||
|
transformReply: undefined as unknown as () => NumberReply
|
||||||
|
} as const satisfies Command;
|
42
packages/client/lib/commands/VEMB.spec.ts
Normal file
42
packages/client/lib/commands/VEMB.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VEMB from './VEMB';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VEMB', () => {
|
||||||
|
it('parseCommand', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VEMB.parseCommand(parser, 'key', 'element');
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VEMB', 'key', 'element']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vEmb', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
|
||||||
|
|
||||||
|
const result = await client.vEmb('key', 'element');
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.equal(result.length, 3);
|
||||||
|
assert.equal(typeof result[0], 'number');
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vEmb with RESP3', async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.5, 2.5, 3.5, 4.5], 'resp3-element');
|
||||||
|
|
||||||
|
const result = await client.vEmb('resp3-key', 'resp3-element');
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.equal(result.length, 4);
|
||||||
|
assert.equal(typeof result[0], 'number');
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
21
packages/client/lib/commands/VEMB.ts
Normal file
21
packages/client/lib/commands/VEMB.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import { RedisArgument, Command } from '../RESP/types';
|
||||||
|
import { transformDoubleArrayReply } from './generic-transformers';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
/**
|
||||||
|
* Retrieve the approximate vector associated with a vector set element
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The key of the vector set
|
||||||
|
* @param element - The name of the element to retrieve the vector for
|
||||||
|
* @see https://redis.io/commands/vemb/
|
||||||
|
*/
|
||||||
|
parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) {
|
||||||
|
parser.push('VEMB');
|
||||||
|
parser.pushKey(key);
|
||||||
|
parser.push(element);
|
||||||
|
},
|
||||||
|
transformReply: transformDoubleArrayReply
|
||||||
|
} as const satisfies Command;
|
68
packages/client/lib/commands/VEMB_RAW.spec.ts
Normal file
68
packages/client/lib/commands/VEMB_RAW.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VEMB_RAW from './VEMB_RAW';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VEMB_RAW', () => {
|
||||||
|
it('parseCommand', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VEMB_RAW.parseCommand(parser, 'key', 'element');
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VEMB', 'key', 'element', 'RAW']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vEmbRaw', async client => {
|
||||||
|
await client.vAdd('key1', [1.0, 2.0, 3.0], 'element');
|
||||||
|
const result1 = await client.vEmbRaw('key1', 'element');
|
||||||
|
assert.equal(result1.quantization, 'int8');
|
||||||
|
assert.ok(result1.quantizationRange !== undefined);
|
||||||
|
|
||||||
|
await client.vAdd('key2', [1.0, 2.0, 3.0], 'element', { QUANT: 'Q8' });
|
||||||
|
const result2 = await client.vEmbRaw('key2', 'element');
|
||||||
|
assert.equal(result2.quantization, 'int8');
|
||||||
|
assert.ok(result2.quantizationRange !== undefined);
|
||||||
|
|
||||||
|
await client.vAdd('key3', [1.0, 2.0, 3.0], 'element', { QUANT: 'NOQUANT' });
|
||||||
|
const result3 = await client.vEmbRaw('key3', 'element');
|
||||||
|
assert.equal(result3.quantization, 'f32');
|
||||||
|
assert.equal(result3.quantizationRange, undefined);
|
||||||
|
|
||||||
|
await client.vAdd('key4', [1.0, 2.0, 3.0], 'element', { QUANT: 'BIN' });
|
||||||
|
const result4 = await client.vEmbRaw('key4', 'element');
|
||||||
|
assert.equal(result4.quantization, 'bin');
|
||||||
|
assert.equal(result4.quantizationRange, undefined);
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vEmbRaw with RESP3', async client => {
|
||||||
|
await client.vAdd('key1', [1.0, 2.0, 3.0], 'element');
|
||||||
|
const result1 = await client.vEmbRaw('key1', 'element');
|
||||||
|
assert.equal(result1.quantization, 'int8');
|
||||||
|
assert.ok(result1.quantizationRange !== undefined);
|
||||||
|
|
||||||
|
await client.vAdd('key2', [1.0, 2.0, 3.0], 'element', { QUANT: 'Q8' });
|
||||||
|
const result2 = await client.vEmbRaw('key2', 'element');
|
||||||
|
assert.equal(result2.quantization, 'int8');
|
||||||
|
assert.ok(result2.quantizationRange !== undefined);
|
||||||
|
|
||||||
|
await client.vAdd('key3', [1.0, 2.0, 3.0], 'element', { QUANT: 'NOQUANT' });
|
||||||
|
const result3 = await client.vEmbRaw('key3', 'element');
|
||||||
|
assert.equal(result3.quantization, 'f32');
|
||||||
|
assert.equal(result3.quantizationRange, undefined);
|
||||||
|
|
||||||
|
await client.vAdd('key4', [1.0, 2.0, 3.0], 'element', { QUANT: 'BIN' });
|
||||||
|
const result4 = await client.vEmbRaw('key4', 'element');
|
||||||
|
assert.equal(result4.quantization, 'bin');
|
||||||
|
assert.equal(result4.quantizationRange, undefined);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
57
packages/client/lib/commands/VEMB_RAW.ts
Normal file
57
packages/client/lib/commands/VEMB_RAW.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import {
|
||||||
|
RedisArgument,
|
||||||
|
Command,
|
||||||
|
BlobStringReply,
|
||||||
|
SimpleStringReply,
|
||||||
|
DoubleReply
|
||||||
|
} from '../RESP/types';
|
||||||
|
import { transformDoubleReply } from './generic-transformers';
|
||||||
|
import VEMB from './VEMB';
|
||||||
|
|
||||||
|
type RawVembReply = {
|
||||||
|
quantization: SimpleStringReply;
|
||||||
|
raw: BlobStringReply;
|
||||||
|
l2Norm: DoubleReply;
|
||||||
|
quantizationRange?: DoubleReply;
|
||||||
|
};
|
||||||
|
|
||||||
|
const transformRawVembReply = {
|
||||||
|
2: (reply: any[]): RawVembReply => {
|
||||||
|
return {
|
||||||
|
quantization: reply[0],
|
||||||
|
raw: reply[1],
|
||||||
|
l2Norm: transformDoubleReply[2](reply[2]),
|
||||||
|
...(reply[3] !== undefined && { quantizationRange: transformDoubleReply[2](reply[3]) })
|
||||||
|
};
|
||||||
|
},
|
||||||
|
3: (reply: any[]): RawVembReply => {
|
||||||
|
return {
|
||||||
|
quantization: reply[0],
|
||||||
|
raw: reply[1],
|
||||||
|
l2Norm: reply[2],
|
||||||
|
quantizationRange: reply[3]
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
/**
|
||||||
|
* Retrieve the RAW approximate vector associated with a vector set element
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The key of the vector set
|
||||||
|
* @param element - The name of the element to retrieve the vector for
|
||||||
|
* @see https://redis.io/commands/vemb/
|
||||||
|
*/
|
||||||
|
parseCommand(
|
||||||
|
parser: CommandParser,
|
||||||
|
key: RedisArgument,
|
||||||
|
element: RedisArgument
|
||||||
|
) {
|
||||||
|
VEMB.parseCommand(parser, key, element);
|
||||||
|
parser.push('RAW');
|
||||||
|
},
|
||||||
|
transformReply: transformRawVembReply
|
||||||
|
} as const satisfies Command;
|
77
packages/client/lib/commands/VGETATTR.spec.ts
Normal file
77
packages/client/lib/commands/VGETATTR.spec.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VGETATTR from './VGETATTR';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VGETATTR', () => {
|
||||||
|
it('parseCommand', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VGETATTR.parseCommand(parser, 'key', 'element');
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VGETATTR', 'key', 'element']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vGetAttr', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
|
||||||
|
|
||||||
|
const nullResult = await client.vGetAttr('key', 'element');
|
||||||
|
assert.equal(nullResult, null);
|
||||||
|
|
||||||
|
await client.vSetAttr('key', 'element', { name: 'test' });
|
||||||
|
|
||||||
|
const result = await client.vGetAttr('key', 'element');
|
||||||
|
|
||||||
|
assert.ok(result !== null);
|
||||||
|
assert.equal(typeof result, 'object')
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
name: 'test'
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vGetAttr with RESP3', async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0], 'resp3-element');
|
||||||
|
|
||||||
|
// Test null case (no attributes set)
|
||||||
|
const nullResult = await client.vGetAttr('resp3-key', 'resp3-element');
|
||||||
|
|
||||||
|
assert.equal(nullResult, null);
|
||||||
|
|
||||||
|
// Set complex attributes and retrieve them
|
||||||
|
const complexAttrs = {
|
||||||
|
name: 'test-item',
|
||||||
|
category: 'electronics',
|
||||||
|
price: 99.99,
|
||||||
|
inStock: true,
|
||||||
|
tags: ['new', 'featured']
|
||||||
|
};
|
||||||
|
await client.vSetAttr('resp3-key', 'resp3-element', complexAttrs);
|
||||||
|
|
||||||
|
const result = await client.vGetAttr('resp3-key', 'resp3-element');
|
||||||
|
|
||||||
|
assert.ok(result !== null);
|
||||||
|
assert.equal(typeof result, 'object')
|
||||||
|
|
||||||
|
assert.deepEqual(result, {
|
||||||
|
name: 'test-item',
|
||||||
|
category: 'electronics',
|
||||||
|
price: 99.99,
|
||||||
|
inStock: true,
|
||||||
|
tags: ['new', 'featured']
|
||||||
|
})
|
||||||
|
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
21
packages/client/lib/commands/VGETATTR.ts
Normal file
21
packages/client/lib/commands/VGETATTR.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import { RedisArgument, Command } from '../RESP/types';
|
||||||
|
import { transformRedisJsonNullReply } from './generic-transformers';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
/**
|
||||||
|
* Retrieve the attributes of a vector set element
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The key of the vector set
|
||||||
|
* @param element - The name of the element to retrieve attributes for
|
||||||
|
* @see https://redis.io/commands/vgetattr/
|
||||||
|
*/
|
||||||
|
parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) {
|
||||||
|
parser.push('VGETATTR');
|
||||||
|
parser.pushKey(key);
|
||||||
|
parser.push(element);
|
||||||
|
},
|
||||||
|
transformReply: transformRedisJsonNullReply
|
||||||
|
} as const satisfies Command;
|
58
packages/client/lib/commands/VINFO.spec.ts
Normal file
58
packages/client/lib/commands/VINFO.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VINFO from './VINFO';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VINFO', () => {
|
||||||
|
it('parseCommand', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VINFO.parseCommand(parser, 'key');
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VINFO', 'key']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vInfo', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
|
||||||
|
|
||||||
|
const result = await client.vInfo('key');
|
||||||
|
assert.ok(typeof result === 'object' && result !== null);
|
||||||
|
|
||||||
|
assert.equal(result['vector-dim'], 3);
|
||||||
|
assert.equal(result['size'], 1);
|
||||||
|
assert.ok('quant-type' in result);
|
||||||
|
assert.ok('hnsw-m' in result);
|
||||||
|
assert.ok('projection-input-dim' in result);
|
||||||
|
assert.ok('max-level' in result);
|
||||||
|
assert.ok('attributes-count' in result);
|
||||||
|
assert.ok('vset-uid' in result);
|
||||||
|
assert.ok('hnsw-max-node-uid' in result);
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vInfo with RESP3', async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element');
|
||||||
|
|
||||||
|
const result = await client.vInfo('resp3-key');
|
||||||
|
assert.ok(typeof result === 'object' && result !== null);
|
||||||
|
|
||||||
|
assert.equal(result['vector-dim'], 3);
|
||||||
|
assert.equal(result['size'], 1);
|
||||||
|
assert.ok('quant-type' in result);
|
||||||
|
assert.ok('hnsw-m' in result);
|
||||||
|
assert.ok('projection-input-dim' in result);
|
||||||
|
assert.ok('max-level' in result);
|
||||||
|
assert.ok('attributes-count' in result);
|
||||||
|
assert.ok('vset-uid' in result);
|
||||||
|
assert.ok('hnsw-max-node-uid' in result);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
38
packages/client/lib/commands/VINFO.ts
Normal file
38
packages/client/lib/commands/VINFO.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import { RedisArgument, Command, UnwrapReply, Resp2Reply, TuplesToMapReply, SimpleStringReply, NumberReply } from '../RESP/types';
|
||||||
|
|
||||||
|
export type VInfoReplyMap = TuplesToMapReply<[
|
||||||
|
[SimpleStringReply<'quant-type'>, SimpleStringReply],
|
||||||
|
[SimpleStringReply<'vector-dim'>, NumberReply],
|
||||||
|
[SimpleStringReply<'size'>, NumberReply],
|
||||||
|
[SimpleStringReply<'max-level'>, NumberReply],
|
||||||
|
[SimpleStringReply<'vset-uid'>, NumberReply],
|
||||||
|
[SimpleStringReply<'hnsw-max-node-uid'>, NumberReply],
|
||||||
|
]>;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
/**
|
||||||
|
* Retrieve metadata and internal details about a vector set, including size, dimensions, quantization type, and graph structure
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The key of the vector set
|
||||||
|
* @see https://redis.io/commands/vinfo/
|
||||||
|
*/
|
||||||
|
parseCommand(parser: CommandParser, key: RedisArgument) {
|
||||||
|
parser.push('VINFO');
|
||||||
|
parser.pushKey(key);
|
||||||
|
},
|
||||||
|
transformReply: {
|
||||||
|
2: (reply: UnwrapReply<Resp2Reply<VInfoReplyMap>>): VInfoReplyMap => {
|
||||||
|
const ret = Object.create(null);
|
||||||
|
|
||||||
|
for (let i = 0; i < reply.length; i += 2) {
|
||||||
|
ret[reply[i].toString()] = reply[i + 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret as unknown as VInfoReplyMap;
|
||||||
|
},
|
||||||
|
3: undefined as unknown as () => VInfoReplyMap
|
||||||
|
}
|
||||||
|
} as const satisfies Command;
|
42
packages/client/lib/commands/VLINKS.spec.ts
Normal file
42
packages/client/lib/commands/VLINKS.spec.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VLINKS from './VLINKS';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VLINKS', () => {
|
||||||
|
it('parseCommand', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VLINKS.parseCommand(parser, 'key', 'element');
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VLINKS', 'key', 'element']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vLinks', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('key', [1.1, 2.1, 3.1], 'element2');
|
||||||
|
|
||||||
|
const result = await client.vLinks('key', 'element1');
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.ok(result.length)
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vLinks with RESP3', async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2');
|
||||||
|
|
||||||
|
const result = await client.vLinks('resp3-key', 'element1');
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.ok(result.length)
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
20
packages/client/lib/commands/VLINKS.ts
Normal file
20
packages/client/lib/commands/VLINKS.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
/**
|
||||||
|
* Retrieve the neighbors of a specified element in a vector set; the connections for each layer of the HNSW graph
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The key of the vector set
|
||||||
|
* @param element - The name of the element to retrieve neighbors for
|
||||||
|
* @see https://redis.io/commands/vlinks/
|
||||||
|
*/
|
||||||
|
parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) {
|
||||||
|
parser.push('VLINKS');
|
||||||
|
parser.pushKey(key);
|
||||||
|
parser.push(element);
|
||||||
|
},
|
||||||
|
transformReply: undefined as unknown as () => ArrayReply<ArrayReply<BlobStringReply>>
|
||||||
|
} as const satisfies Command;
|
75
packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts
Normal file
75
packages/client/lib/commands/VLINKS_WITHSCORES.spec.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VLINKS_WITHSCORES from './VLINKS_WITHSCORES';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VLINKS WITHSCORES', () => {
|
||||||
|
it('parseCommand', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VLINKS_WITHSCORES.parseCommand(parser, 'key', 'element');
|
||||||
|
assert.deepEqual(parser.redisArgs, [
|
||||||
|
'VLINKS',
|
||||||
|
'key',
|
||||||
|
'element',
|
||||||
|
'WITHSCORES'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll(
|
||||||
|
'vLinksWithScores',
|
||||||
|
async client => {
|
||||||
|
// Create a vector set with multiple elements to build HNSW graph layers
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('key', [1.1, 2.1, 3.1], 'element2');
|
||||||
|
await client.vAdd('key', [1.2, 2.2, 3.2], 'element3');
|
||||||
|
await client.vAdd('key', [2.0, 3.0, 4.0], 'element4');
|
||||||
|
|
||||||
|
const result = await client.vLinksWithScores('key', 'element1');
|
||||||
|
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
|
||||||
|
for (const layer of result) {
|
||||||
|
assert.equal(
|
||||||
|
typeof layer,
|
||||||
|
'object'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(result.length >= 1, 'Should have at least layer 0');
|
||||||
|
},
|
||||||
|
{
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
testUtils.testWithClient(
|
||||||
|
'vLinksWithScores with RESP3',
|
||||||
|
async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2');
|
||||||
|
await client.vAdd('resp3-key', [1.2, 2.2, 3.2], 'element3');
|
||||||
|
await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element4');
|
||||||
|
|
||||||
|
const result = await client.vLinksWithScores('resp3-key', 'element1');
|
||||||
|
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
|
||||||
|
for (const layer of result) {
|
||||||
|
assert.equal(
|
||||||
|
typeof layer,
|
||||||
|
'object'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.ok(result.length >= 1, 'Should have at least layer 0');
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
42
packages/client/lib/commands/VLINKS_WITHSCORES.ts
Normal file
42
packages/client/lib/commands/VLINKS_WITHSCORES.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { BlobStringReply, Command, DoubleReply, MapReply } from '../RESP/types';
|
||||||
|
import { transformDoubleReply } from './generic-transformers';
|
||||||
|
import VLINKS from './VLINKS';
|
||||||
|
|
||||||
|
|
||||||
|
function transformVLinksWithScoresReply(reply: any): Array<Record<string, DoubleReply>> {
|
||||||
|
const layers: Array<Record<string, DoubleReply>> = [];
|
||||||
|
|
||||||
|
for (const layer of reply) {
|
||||||
|
const obj: Record<string, DoubleReply> = Object.create(null);
|
||||||
|
|
||||||
|
// Each layer contains alternating element names and scores
|
||||||
|
for (let i = 0; i < layer.length; i += 2) {
|
||||||
|
const element = layer[i];
|
||||||
|
const score = transformDoubleReply[2](layer[i + 1]);
|
||||||
|
obj[element.toString()] = score;
|
||||||
|
}
|
||||||
|
|
||||||
|
layers.push(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
return layers;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_READ_ONLY: VLINKS.IS_READ_ONLY,
|
||||||
|
/**
|
||||||
|
* Get the connections for each layer of the HNSW graph with similarity scores
|
||||||
|
* @param args - Same parameters as the VLINKS command
|
||||||
|
* @see https://redis.io/commands/vlinks/
|
||||||
|
*/
|
||||||
|
parseCommand(...args: Parameters<typeof VLINKS.parseCommand>) {
|
||||||
|
const parser = args[0];
|
||||||
|
|
||||||
|
VLINKS.parseCommand(...args);
|
||||||
|
parser.push('WITHSCORES');
|
||||||
|
},
|
||||||
|
transformReply: {
|
||||||
|
2: transformVLinksWithScoresReply,
|
||||||
|
3: undefined as unknown as () => Array<MapReply<BlobStringReply, DoubleReply>>
|
||||||
|
}
|
||||||
|
} as const satisfies Command;
|
201
packages/client/lib/commands/VRANDMEMBER.spec.ts
Normal file
201
packages/client/lib/commands/VRANDMEMBER.spec.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VRANDMEMBER from './VRANDMEMBER';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VRANDMEMBER', () => {
|
||||||
|
describe('parseCommand', () => {
|
||||||
|
it('without count', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VRANDMEMBER.parseCommand(parser, 'key');
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VRANDMEMBER', 'key']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with count', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VRANDMEMBER.parseCommand(parser, 'key', 2);
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VRANDMEMBER', 'key', '2']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RESP2 tests', () => {
|
||||||
|
testUtils.testAll('vRandMember without count - returns single element as string', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('key', [4.0, 5.0, 6.0], 'element2');
|
||||||
|
await client.vAdd('key', [7.0, 8.0, 9.0], 'element3');
|
||||||
|
|
||||||
|
const result = await client.vRandMember('key');
|
||||||
|
assert.equal(typeof result, 'string');
|
||||||
|
assert.ok(['element1', 'element2', 'element3'].includes(result as string));
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vRandMember with positive count - returns distinct elements', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('key', [4.0, 5.0, 6.0], 'element2');
|
||||||
|
await client.vAdd('key', [7.0, 8.0, 9.0], 'element3');
|
||||||
|
|
||||||
|
const result = await client.vRandMember('key', 2);
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.equal(result.length, 2);
|
||||||
|
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vRandMember with negative count - allows duplicates', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('key', [4.0, 5.0, 6.0], 'element2');
|
||||||
|
|
||||||
|
const result = await client.vRandMember('key', -5);
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.equal(result.length, 5);
|
||||||
|
|
||||||
|
// All elements should be from our set (duplicates allowed)
|
||||||
|
result.forEach(element => {
|
||||||
|
assert.ok(['element1', 'element2'].includes(element));
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vRandMember count exceeds set size - returns entire set', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('key', [4.0, 5.0, 6.0], 'element2');
|
||||||
|
|
||||||
|
const result = await client.vRandMember('key', 10);
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.equal(result.length, 2); // Only 2 elements exist
|
||||||
|
|
||||||
|
// Should contain both elements
|
||||||
|
assert.ok(result.includes('element1'));
|
||||||
|
assert.ok(result.includes('element2'));
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vRandMember on non-existent key', async client => {
|
||||||
|
// Without count - should return null
|
||||||
|
const resultNoCount = await client.vRandMember('nonexistent');
|
||||||
|
assert.equal(resultNoCount, null);
|
||||||
|
|
||||||
|
// With count - should return empty array
|
||||||
|
const resultWithCount = await client.vRandMember('nonexistent', 5);
|
||||||
|
assert.ok(Array.isArray(resultWithCount));
|
||||||
|
assert.equal(resultWithCount.length, 0);
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('RESP3 tests', () => {
|
||||||
|
testUtils.testWithClient('vRandMember without count - returns single element as string', async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2');
|
||||||
|
await client.vAdd('resp3-key', [7.0, 8.0, 9.0], 'element3');
|
||||||
|
|
||||||
|
const result = await client.vRandMember('resp3-key');
|
||||||
|
assert.equal(typeof result, 'string');
|
||||||
|
assert.ok(['element1', 'element2', 'element3'].includes(result as string));
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vRandMember with positive count - returns distinct elements', async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2');
|
||||||
|
await client.vAdd('resp3-key', [7.0, 8.0, 9.0], 'element3');
|
||||||
|
|
||||||
|
const result = await client.vRandMember('resp3-key', 2);
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.equal(result.length, 2);
|
||||||
|
|
||||||
|
// Should be distinct elements (no duplicates)
|
||||||
|
const uniqueElements = new Set(result);
|
||||||
|
assert.equal(uniqueElements.size, 2);
|
||||||
|
|
||||||
|
// All elements should be from our set
|
||||||
|
result.forEach(element => {
|
||||||
|
assert.ok(['element1', 'element2', 'element3'].includes(element));
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vRandMember with negative count - allows duplicates', async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2');
|
||||||
|
|
||||||
|
const result = await client.vRandMember('resp3-key', -5);
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.equal(result.length, 5);
|
||||||
|
|
||||||
|
// All elements should be from our set (duplicates allowed)
|
||||||
|
result.forEach(element => {
|
||||||
|
assert.ok(['element1', 'element2'].includes(element));
|
||||||
|
});
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vRandMember count exceeds set size - returns entire set', async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('resp3-key', [4.0, 5.0, 6.0], 'element2');
|
||||||
|
|
||||||
|
const result = await client.vRandMember('resp3-key', 10);
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.equal(result.length, 2); // Only 2 elements exist
|
||||||
|
|
||||||
|
// Should contain both elements
|
||||||
|
assert.ok(result.includes('element1'));
|
||||||
|
assert.ok(result.includes('element2'));
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vRandMember on non-existent key', async client => {
|
||||||
|
// Without count - should return null
|
||||||
|
const resultNoCount = await client.vRandMember('resp3-nonexistent');
|
||||||
|
assert.equal(resultNoCount, null);
|
||||||
|
|
||||||
|
// With count - should return empty array
|
||||||
|
const resultWithCount = await client.vRandMember('resp3-nonexistent', 5);
|
||||||
|
assert.ok(Array.isArray(resultWithCount));
|
||||||
|
assert.equal(resultWithCount.length, 0);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
23
packages/client/lib/commands/VRANDMEMBER.ts
Normal file
23
packages/client/lib/commands/VRANDMEMBER.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import { RedisArgument, BlobStringReply, ArrayReply, Command, NullReply } from '../RESP/types';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
/**
|
||||||
|
* Retrieve random elements of a vector set
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The key of the vector set
|
||||||
|
* @param count - Optional number of elements to return
|
||||||
|
* @see https://redis.io/commands/vrandmember/
|
||||||
|
*/
|
||||||
|
parseCommand(parser: CommandParser, key: RedisArgument, count?: number) {
|
||||||
|
parser.push('VRANDMEMBER');
|
||||||
|
parser.pushKey(key);
|
||||||
|
|
||||||
|
if (count !== undefined) {
|
||||||
|
parser.push(count.toString());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transformReply: undefined as unknown as () => BlobStringReply | ArrayReply<BlobStringReply> | NullReply
|
||||||
|
} as const satisfies Command;
|
63
packages/client/lib/commands/VREM.spec.ts
Normal file
63
packages/client/lib/commands/VREM.spec.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VREM from './VREM';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VREM', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VREM.parseCommand(parser, 'key', 'element');
|
||||||
|
it('parseCommand', () => {
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VREM', 'key', 'element']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vRem', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.vRem('key', 'element'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.vRem('key', 'element'),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.vCard('key'),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vRem with RESP3', async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.vRem('resp3-key', 'resp3-element'),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.vRem('resp3-key', 'resp3-element'),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.vCard('resp3-key'),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
20
packages/client/lib/commands/VREM.ts
Normal file
20
packages/client/lib/commands/VREM.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import { RedisArgument, Command } from '../RESP/types';
|
||||||
|
import { transformBooleanReply } from './generic-transformers';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Remove an element from a vector set
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The key of the vector set
|
||||||
|
* @param element - The name of the element to remove from the vector set
|
||||||
|
* @see https://redis.io/commands/vrem/
|
||||||
|
*/
|
||||||
|
parseCommand(parser: CommandParser, key: RedisArgument, element: RedisArgument) {
|
||||||
|
parser.push('VREM');
|
||||||
|
parser.pushKey(key);
|
||||||
|
parser.push(element);
|
||||||
|
},
|
||||||
|
transformReply: transformBooleanReply
|
||||||
|
} as const satisfies Command;
|
58
packages/client/lib/commands/VSETATTR.spec.ts
Normal file
58
packages/client/lib/commands/VSETATTR.spec.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VSETATTR from './VSETATTR';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VSETATTR', () => {
|
||||||
|
describe('parseCommand', () => {
|
||||||
|
it('with object', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VSETATTR.parseCommand(parser, 'key', 'element', { name: 'test', value: 42 }),
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VSETATTR', 'key', 'element', '{"name":"test","value":42}']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with string', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VSETATTR.parseCommand(parser, 'key', 'element', '{"name":"test"}'),
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VSETATTR', 'key', 'element', '{"name":"test"}']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vSetAttr', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element');
|
||||||
|
|
||||||
|
assert.equal(
|
||||||
|
await client.vSetAttr('key', 'element', { name: 'test', value: 42 }),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vSetAttr with RESP3 - returns boolean', async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'resp3-element');
|
||||||
|
|
||||||
|
const result = await client.vSetAttr('resp3-key', 'resp3-element', {
|
||||||
|
name: 'test-item',
|
||||||
|
category: 'electronics',
|
||||||
|
price: 99.99
|
||||||
|
});
|
||||||
|
|
||||||
|
// RESP3 returns boolean instead of number
|
||||||
|
assert.equal(typeof result, 'boolean');
|
||||||
|
assert.equal(result, true);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
32
packages/client/lib/commands/VSETATTR.ts
Normal file
32
packages/client/lib/commands/VSETATTR.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import { RedisArgument, Command } from '../RESP/types';
|
||||||
|
import { transformBooleanReply } from './generic-transformers';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* Set or replace attributes on a vector set element
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The key of the vector set
|
||||||
|
* @param element - The name of the element to set attributes for
|
||||||
|
* @param attributes - The attributes to set (as JSON string or object)
|
||||||
|
* @see https://redis.io/commands/vsetattr/
|
||||||
|
*/
|
||||||
|
parseCommand(
|
||||||
|
parser: CommandParser,
|
||||||
|
key: RedisArgument,
|
||||||
|
element: RedisArgument,
|
||||||
|
attributes: RedisArgument | Record<string, any>
|
||||||
|
) {
|
||||||
|
parser.push('VSETATTR');
|
||||||
|
parser.pushKey(key);
|
||||||
|
parser.push(element);
|
||||||
|
|
||||||
|
if (typeof attributes === 'object' && attributes !== null) {
|
||||||
|
parser.push(JSON.stringify(attributes));
|
||||||
|
} else {
|
||||||
|
parser.push(attributes);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transformReply: transformBooleanReply
|
||||||
|
} as const satisfies Command;
|
85
packages/client/lib/commands/VSIM.spec.ts
Normal file
85
packages/client/lib/commands/VSIM.spec.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VSIM from './VSIM';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VSIM', () => {
|
||||||
|
describe('parseCommand', () => {
|
||||||
|
it('with vector', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VSIM.parseCommand(parser, 'key', [1.0, 2.0, 3.0]),
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VSIM', 'key', 'VALUES', '3', '1', '2', '3']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with element', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VSIM.parseCommand(parser, 'key', 'element');
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
['VSIM', 'key', 'ELE', 'element']
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with options', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VSIM.parseCommand(parser, 'key', 'element', {
|
||||||
|
COUNT: 5,
|
||||||
|
EF: 100,
|
||||||
|
FILTER: '.price > 20',
|
||||||
|
'FILTER-EF': 50,
|
||||||
|
TRUTH: true,
|
||||||
|
NOTHREAD: true
|
||||||
|
});
|
||||||
|
assert.deepEqual(
|
||||||
|
parser.redisArgs,
|
||||||
|
[
|
||||||
|
'VSIM', 'key', 'ELE', 'element',
|
||||||
|
'COUNT', '5', 'EF', '100', 'FILTER', '.price > 20',
|
||||||
|
'FILTER-EF', '50', 'TRUTH', 'NOTHREAD'
|
||||||
|
]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll('vSim', async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('key', [1.1, 2.1, 3.1], 'element2');
|
||||||
|
|
||||||
|
const result = await client.vSim('key', 'element1');
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.ok(result.includes('element1'));
|
||||||
|
}, {
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testWithClient('vSim with RESP3', async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2');
|
||||||
|
await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element3');
|
||||||
|
|
||||||
|
// Test similarity search with vector
|
||||||
|
const resultWithVector = await client.vSim('resp3-key', [1.05, 2.05, 3.05]);
|
||||||
|
assert.ok(Array.isArray(resultWithVector));
|
||||||
|
assert.ok(resultWithVector.length > 0);
|
||||||
|
|
||||||
|
// Test similarity search with element
|
||||||
|
const resultWithElement = await client.vSim('resp3-key', 'element1');
|
||||||
|
assert.ok(Array.isArray(resultWithElement));
|
||||||
|
assert.ok(resultWithElement.includes('element1'));
|
||||||
|
|
||||||
|
// Test with options
|
||||||
|
const resultWithOptions = await client.vSim('resp3-key', 'element1', { COUNT: 2 });
|
||||||
|
assert.ok(Array.isArray(resultWithOptions));
|
||||||
|
assert.ok(resultWithOptions.length <= 2);
|
||||||
|
}, {
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
});
|
||||||
|
});
|
68
packages/client/lib/commands/VSIM.ts
Normal file
68
packages/client/lib/commands/VSIM.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { CommandParser } from '../client/parser';
|
||||||
|
import { RedisArgument, ArrayReply, BlobStringReply, Command } from '../RESP/types';
|
||||||
|
import { transformDoubleArgument } from './generic-transformers';
|
||||||
|
|
||||||
|
export interface VSimOptions {
|
||||||
|
COUNT?: number;
|
||||||
|
EF?: number;
|
||||||
|
FILTER?: string;
|
||||||
|
'FILTER-EF'?: number;
|
||||||
|
TRUTH?: boolean;
|
||||||
|
NOTHREAD?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_READ_ONLY: true,
|
||||||
|
/**
|
||||||
|
* Retrieve elements similar to a given vector or element with optional filtering
|
||||||
|
*
|
||||||
|
* @param parser - The command parser
|
||||||
|
* @param key - The key of the vector set
|
||||||
|
* @param query - The query vector (array of numbers) or element name (string)
|
||||||
|
* @param options - Optional parameters for similarity search
|
||||||
|
* @see https://redis.io/commands/vsim/
|
||||||
|
*/
|
||||||
|
parseCommand(
|
||||||
|
parser: CommandParser,
|
||||||
|
key: RedisArgument,
|
||||||
|
query: RedisArgument | Array<number>,
|
||||||
|
options?: VSimOptions
|
||||||
|
) {
|
||||||
|
parser.push('VSIM');
|
||||||
|
parser.pushKey(key);
|
||||||
|
|
||||||
|
if (Array.isArray(query)) {
|
||||||
|
parser.push('VALUES', query.length.toString());
|
||||||
|
for (const value of query) {
|
||||||
|
parser.push(transformDoubleArgument(value));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parser.push('ELE', query);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.COUNT !== undefined) {
|
||||||
|
parser.push('COUNT', options.COUNT.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.EF !== undefined) {
|
||||||
|
parser.push('EF', options.EF.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.FILTER) {
|
||||||
|
parser.push('FILTER', options.FILTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.['FILTER-EF'] !== undefined) {
|
||||||
|
parser.push('FILTER-EF', options['FILTER-EF'].toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.TRUTH) {
|
||||||
|
parser.push('TRUTH');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options?.NOTHREAD) {
|
||||||
|
parser.push('NOTHREAD');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
|
||||||
|
} as const satisfies Command;
|
62
packages/client/lib/commands/VSIM_WITHSCORES.spec.ts
Normal file
62
packages/client/lib/commands/VSIM_WITHSCORES.spec.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import testUtils, { GLOBAL } from '../test-utils';
|
||||||
|
import VSIM_WITHSCORES from './VSIM_WITHSCORES';
|
||||||
|
import { BasicCommandParser } from '../client/parser';
|
||||||
|
|
||||||
|
describe('VSIM WITHSCORES', () => {
|
||||||
|
it('parseCommand', () => {
|
||||||
|
const parser = new BasicCommandParser();
|
||||||
|
VSIM_WITHSCORES.parseCommand(parser, 'key', 'element')
|
||||||
|
assert.deepEqual(parser.redisArgs, [
|
||||||
|
'VSIM',
|
||||||
|
'key',
|
||||||
|
'ELE',
|
||||||
|
'element',
|
||||||
|
'WITHSCORES'
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUtils.testAll(
|
||||||
|
'vSimWithScores',
|
||||||
|
async client => {
|
||||||
|
await client.vAdd('key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('key', [1.1, 2.1, 3.1], 'element2');
|
||||||
|
|
||||||
|
const result = await client.vSimWithScores('key', 'element1');
|
||||||
|
|
||||||
|
assert.ok(typeof result === 'object');
|
||||||
|
assert.ok('element1' in result);
|
||||||
|
assert.ok('element2' in result);
|
||||||
|
assert.equal(typeof result['element1'], 'number');
|
||||||
|
assert.equal(typeof result['element2'], 'number');
|
||||||
|
},
|
||||||
|
{
|
||||||
|
client: { ...GLOBAL.SERVERS.OPEN, minimumDockerVersion: [8, 0] },
|
||||||
|
cluster: { ...GLOBAL.CLUSTERS.OPEN, minimumDockerVersion: [8, 0] }
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
testUtils.testWithClient(
|
||||||
|
'vSimWithScores with RESP3 - returns Map with scores',
|
||||||
|
async client => {
|
||||||
|
await client.vAdd('resp3-key', [1.0, 2.0, 3.0], 'element1');
|
||||||
|
await client.vAdd('resp3-key', [1.1, 2.1, 3.1], 'element2');
|
||||||
|
await client.vAdd('resp3-key', [2.0, 3.0, 4.0], 'element3');
|
||||||
|
|
||||||
|
const result = await client.vSimWithScores('resp3-key', 'element1');
|
||||||
|
|
||||||
|
assert.ok(typeof result === 'object');
|
||||||
|
assert.ok('element1' in result);
|
||||||
|
assert.ok('element2' in result);
|
||||||
|
assert.equal(typeof result['element1'], 'number');
|
||||||
|
assert.equal(typeof result['element2'], 'number');
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...GLOBAL.SERVERS.OPEN,
|
||||||
|
clientOptions: {
|
||||||
|
RESP: 3
|
||||||
|
},
|
||||||
|
minimumDockerVersion: [8, 0]
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
36
packages/client/lib/commands/VSIM_WITHSCORES.ts
Normal file
36
packages/client/lib/commands/VSIM_WITHSCORES.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import {
|
||||||
|
ArrayReply,
|
||||||
|
BlobStringReply,
|
||||||
|
Command,
|
||||||
|
DoubleReply,
|
||||||
|
MapReply,
|
||||||
|
UnwrapReply
|
||||||
|
} from '../RESP/types';
|
||||||
|
import { transformDoubleReply } from './generic-transformers';
|
||||||
|
import VSIM from './VSIM';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
IS_READ_ONLY: VSIM.IS_READ_ONLY,
|
||||||
|
/**
|
||||||
|
* Retrieve elements similar to a given vector or element with similarity scores
|
||||||
|
* @param args - Same parameters as the VSIM command
|
||||||
|
* @see https://redis.io/commands/vsim/
|
||||||
|
*/
|
||||||
|
parseCommand(...args: Parameters<typeof VSIM.parseCommand>) {
|
||||||
|
const parser = args[0];
|
||||||
|
|
||||||
|
VSIM.parseCommand(...args);
|
||||||
|
parser.push('WITHSCORES');
|
||||||
|
},
|
||||||
|
transformReply: {
|
||||||
|
2: (reply: ArrayReply<BlobStringReply>) => {
|
||||||
|
const inferred = reply as unknown as UnwrapReply<typeof reply>;
|
||||||
|
const members: Record<string, DoubleReply> = {};
|
||||||
|
for (let i = 0; i < inferred.length; i += 2) {
|
||||||
|
members[inferred[i].toString()] = transformDoubleReply[2](inferred[i + 1]);
|
||||||
|
}
|
||||||
|
return members;
|
||||||
|
},
|
||||||
|
3: undefined as unknown as () => MapReply<BlobStringReply, DoubleReply>
|
||||||
|
}
|
||||||
|
} as const satisfies Command;
|
@@ -662,3 +662,21 @@ export function transformStreamsMessagesReplyResp3(reply: UnwrapReply<StreamsMes
|
|||||||
return ret as unknown as MapReply<BlobStringReply, StreamMessagesReply>
|
return ret as unknown as MapReply<BlobStringReply, StreamMessagesReply>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type RedisJSON = null | boolean | number | string | Date | Array<RedisJSON> | {
|
||||||
|
[key: string]: RedisJSON;
|
||||||
|
[key: number]: RedisJSON;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function transformRedisJsonArgument(json: RedisJSON): string {
|
||||||
|
return JSON.stringify(json);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformRedisJsonReply(json: BlobStringReply): RedisJSON {
|
||||||
|
const res = JSON.parse((json as unknown as UnwrapReply<typeof json>).toString());
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON {
|
||||||
|
return isNullReply(json) ? json : transformRedisJsonReply(json);
|
||||||
|
}
|
||||||
|
@@ -344,6 +344,20 @@ import ZSCORE from './ZSCORE';
|
|||||||
import ZUNION_WITHSCORES from './ZUNION_WITHSCORES';
|
import ZUNION_WITHSCORES from './ZUNION_WITHSCORES';
|
||||||
import ZUNION from './ZUNION';
|
import ZUNION from './ZUNION';
|
||||||
import ZUNIONSTORE from './ZUNIONSTORE';
|
import ZUNIONSTORE from './ZUNIONSTORE';
|
||||||
|
import VADD from './VADD';
|
||||||
|
import VCARD from './VCARD';
|
||||||
|
import VDIM from './VDIM';
|
||||||
|
import VEMB from './VEMB';
|
||||||
|
import VEMB_RAW from './VEMB_RAW';
|
||||||
|
import VGETATTR from './VGETATTR';
|
||||||
|
import VINFO from './VINFO';
|
||||||
|
import VLINKS from './VLINKS';
|
||||||
|
import VLINKS_WITHSCORES from './VLINKS_WITHSCORES';
|
||||||
|
import VRANDMEMBER from './VRANDMEMBER';
|
||||||
|
import VREM from './VREM';
|
||||||
|
import VSETATTR from './VSETATTR';
|
||||||
|
import VSIM from './VSIM';
|
||||||
|
import VSIM_WITHSCORES from './VSIM_WITHSCORES';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ACL_CAT,
|
ACL_CAT,
|
||||||
@@ -1037,5 +1051,33 @@ export default {
|
|||||||
ZUNION,
|
ZUNION,
|
||||||
zUnion: ZUNION,
|
zUnion: ZUNION,
|
||||||
ZUNIONSTORE,
|
ZUNIONSTORE,
|
||||||
zUnionStore: ZUNIONSTORE
|
zUnionStore: ZUNIONSTORE,
|
||||||
|
VADD,
|
||||||
|
vAdd: VADD,
|
||||||
|
VCARD,
|
||||||
|
vCard: VCARD,
|
||||||
|
VDIM,
|
||||||
|
vDim: VDIM,
|
||||||
|
VEMB,
|
||||||
|
vEmb: VEMB,
|
||||||
|
VEMB_RAW,
|
||||||
|
vEmbRaw: VEMB_RAW,
|
||||||
|
VGETATTR,
|
||||||
|
vGetAttr: VGETATTR,
|
||||||
|
VINFO,
|
||||||
|
vInfo: VINFO,
|
||||||
|
VLINKS,
|
||||||
|
vLinks: VLINKS,
|
||||||
|
VLINKS_WITHSCORES,
|
||||||
|
vLinksWithScores: VLINKS_WITHSCORES,
|
||||||
|
VRANDMEMBER,
|
||||||
|
vRandMember: VRANDMEMBER,
|
||||||
|
VREM,
|
||||||
|
vRem: VREM,
|
||||||
|
VSETATTR,
|
||||||
|
vSetAttr: VSETATTR,
|
||||||
|
VSIM,
|
||||||
|
vSim: VSIM,
|
||||||
|
VSIM_WITHSCORES,
|
||||||
|
vSimWithScores: VSIM_WITHSCORES
|
||||||
} as const satisfies RedisCommands;
|
} as const satisfies RedisCommands;
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||||
import { RedisJSON, transformRedisJsonArgument } from './helpers';
|
import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
|
import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||||
import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
|
import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
import { RedisJSON, transformRedisJsonArgument } from './helpers';
|
import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export interface JsonArrIndexOptions {
|
export interface JsonArrIndexOptions {
|
||||||
range?: {
|
range?: {
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||||
import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
|
import { RedisArgument, NumberReply, ArrayReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
import { RedisJSON, transformRedisJsonArgument } from './helpers';
|
import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
IS_READ_ONLY: false,
|
IS_READ_ONLY: false,
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||||
import { RedisArgument, ArrayReply, NullReply, BlobStringReply, Command, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
|
import { RedisArgument, ArrayReply, NullReply, BlobStringReply, Command, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
|
||||||
import { isArrayReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
import { isArrayReply, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
import { transformRedisJsonNullReply } from './helpers';
|
|
||||||
|
|
||||||
export interface RedisArrPopOptions {
|
export interface RedisArrPopOptions {
|
||||||
path: RedisArgument;
|
path: RedisArgument;
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||||
import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
|
import { RedisArgument, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
import { RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
|
import { RedisVariadicArgument, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
import { transformRedisJsonNullReply } from './helpers';
|
|
||||||
|
|
||||||
export interface JsonGetOptions {
|
export interface JsonGetOptions {
|
||||||
path?: RedisVariadicArgument;
|
path?: RedisVariadicArgument;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||||
import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
|
import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
|
||||||
import { RedisJSON, transformRedisJsonArgument } from './helpers';
|
import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
IS_READ_ONLY: false,
|
IS_READ_ONLY: false,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||||
import { RedisArgument, UnwrapReply, ArrayReply, NullReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
|
import { RedisArgument, UnwrapReply, ArrayReply, NullReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
import { transformRedisJsonNullReply } from './helpers';
|
import { transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
IS_READ_ONLY: true,
|
IS_READ_ONLY: true,
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||||
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
|
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
import { RedisJSON, transformRedisJsonArgument } from './helpers';
|
import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export interface JsonMSetItem {
|
export interface JsonMSetItem {
|
||||||
key: RedisArgument;
|
key: RedisArgument;
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||||
import { RedisArgument, SimpleStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
|
import { RedisArgument, SimpleStringReply, NullReply, Command } from '@redis/client/dist/lib/RESP/types';
|
||||||
import { RedisJSON, transformRedisJsonArgument } from './helpers';
|
import { RedisJSON, transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export interface JsonSetOptions {
|
export interface JsonSetOptions {
|
||||||
condition?: 'NX' | 'XX';
|
condition?: 'NX' | 'XX';
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
import { CommandParser } from '@redis/client/dist/lib/client/parser';
|
||||||
import { RedisArgument, Command, NullReply, NumberReply, ArrayReply } from '@redis/client/dist/lib/RESP/types';
|
import { RedisArgument, Command, NullReply, NumberReply, ArrayReply } from '@redis/client/dist/lib/RESP/types';
|
||||||
import { transformRedisJsonArgument } from './helpers';
|
import { transformRedisJsonArgument } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export interface JsonStrAppendOptions {
|
export interface JsonStrAppendOptions {
|
||||||
path?: RedisArgument;
|
path?: RedisArgument;
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
import { isNullReply } from "@redis/client/dist/lib/commands/generic-transformers";
|
|
||||||
import { BlobStringReply, NullReply, UnwrapReply } from "@redis/client/dist/lib/RESP/types";
|
|
||||||
|
|
||||||
export function transformRedisJsonNullReply(json: NullReply | BlobStringReply): NullReply | RedisJSON {
|
|
||||||
return isNullReply(json) ? json : transformRedisJsonReply(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RedisJSON = null | boolean | number | string | Date | Array<RedisJSON> | {
|
|
||||||
[key: string]: RedisJSON;
|
|
||||||
[key: number]: RedisJSON;
|
|
||||||
};
|
|
||||||
|
|
||||||
export function transformRedisJsonArgument(json: RedisJSON): string {
|
|
||||||
return JSON.stringify(json);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transformRedisJsonReply(json: BlobStringReply): RedisJSON {
|
|
||||||
const res = JSON.parse((json as unknown as UnwrapReply<typeof json>).toString());
|
|
||||||
return res;
|
|
||||||
}
|
|
@@ -23,7 +23,9 @@ import STRLEN from './STRLEN';
|
|||||||
import TOGGLE from './TOGGLE';
|
import TOGGLE from './TOGGLE';
|
||||||
import TYPE from './TYPE';
|
import TYPE from './TYPE';
|
||||||
|
|
||||||
export * from './helpers';
|
// Re-export helper types and functions from client package
|
||||||
|
export type { RedisJSON } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
export { transformRedisJsonArgument, transformRedisJsonReply, transformRedisJsonNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
ARRAPPEND,
|
ARRAPPEND,
|
||||||
|
Reference in New Issue
Block a user