1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-04 15:02:09 +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:
Nikolay Karadzhov
2025-06-24 13:35:29 +03:00
committed by GitHub
parent b52177752e
commit c5b4f47975
42 changed files with 1608 additions and 34 deletions

View 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]
});
});

View 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;

View 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]
});
});

View 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;

View 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]
});
});

View 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;

View 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]
});
});

View 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;

View 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]
});
});

View 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;

View 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]
});
});

View 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;

View 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]
});
});

View 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;

View 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]
});
});

View 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;

View 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]
}
);
});

View 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;

View 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]
});
});
});

View 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;

View 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]
});
});

View 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;

View 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]
});
});

View 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;

View 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]
});
});

View 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;

View 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]
}
);
});

View 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;

View File

@@ -662,3 +662,21 @@ export function transformStreamsMessagesReplyResp3(reply: UnwrapReply<StreamsMes
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);
}

View File

@@ -344,6 +344,20 @@ import ZSCORE from './ZSCORE';
import ZUNION_WITHSCORES from './ZUNION_WITHSCORES';
import ZUNION from './ZUNION';
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 {
ACL_CAT,
@@ -1037,5 +1051,33 @@ export default {
ZUNION,
zUnion: ZUNION,
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;