1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-06 02:15:48 +03:00

Merge branch 'master' of github.com:redis/node-redis

This commit is contained in:
leibale
2022-03-31 14:41:59 +03:00
22 changed files with 616 additions and 257 deletions

View File

@@ -17,7 +17,7 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
node-version: ['12', '14', '16'] node-version: ['12', '14', '16']
redis-version: ['5', '6.0', '6.2'] redis-version: ['5', '6.0', '6.2', '7.0-rc2']
steps: steps:
- uses: actions/checkout@v2.3.4 - uses: actions/checkout@v2.3.4
with: with:

View File

@@ -13,20 +13,32 @@ describe('ACL GETUSER', () => {
}); });
testUtils.testWithClient('client.aclGetUser', async client => { testUtils.testWithClient('client.aclGetUser', async client => {
const expectedReply: any = {
passwords: [],
commands: '+@all',
};
if (testUtils.isVersionGreaterThan([7])) {
expectedReply.flags = ['on', 'nopass'];
expectedReply.keys = '~*';
expectedReply.channels = '&*';
expectedReply.selectors = [];
} else {
expectedReply.keys = ['*'];
expectedReply.selectors = undefined;
if (testUtils.isVersionGreaterThan([6, 2])) {
expectedReply.flags = ['on', 'allkeys', 'allchannels', 'allcommands', 'nopass'];
expectedReply.channels = ['*'];
} else {
expectedReply.flags = ['on', 'allkeys', 'allcommands', 'nopass'];
expectedReply.channels = undefined;
}
}
assert.deepEqual( assert.deepEqual(
await client.aclGetUser('default'), await client.aclGetUser('default'),
{ expectedReply
passwords: [],
commands: '+@all',
keys: ['*'],
...(testUtils.isVersionGreaterThan([6, 2]) ? {
flags: ['on', 'allkeys', 'allchannels', 'allcommands', 'nopass'],
channels: ['*']
} : {
flags: ['on', 'allkeys', 'allcommands', 'nopass'],
channels: undefined
})
}
); );
}, GLOBAL.SERVERS.OPEN); }, GLOBAL.SERVERS.OPEN);
}); });

View File

@@ -5,24 +5,27 @@ export function transformArguments(username: RedisCommandArgument): RedisCommand
} }
type AclGetUserRawReply = [ type AclGetUserRawReply = [
_: RedisCommandArgument, 'flags',
flags: Array<RedisCommandArgument>, Array<RedisCommandArgument>,
_: RedisCommandArgument, 'passwords',
passwords: Array<RedisCommandArgument>, Array<RedisCommandArgument>,
_: RedisCommandArgument, 'commands',
commands: RedisCommandArgument, RedisCommandArgument,
_: RedisCommandArgument, 'keys',
keys: Array<RedisCommandArgument>, Array<RedisCommandArgument> | RedisCommandArgument,
_: RedisCommandArgument, 'channels',
channels: Array<RedisCommandArgument> Array<RedisCommandArgument> | RedisCommandArgument,
'selectors' | undefined,
Array<Array<string>> | undefined
]; ];
interface AclUser { interface AclUser {
flags: Array<RedisCommandArgument>; flags: Array<RedisCommandArgument>;
passwords: Array<RedisCommandArgument>; passwords: Array<RedisCommandArgument>;
commands: RedisCommandArgument; commands: RedisCommandArgument;
keys: Array<RedisCommandArgument>; keys: Array<RedisCommandArgument> | RedisCommandArgument;
channels: Array<RedisCommandArgument> channels: Array<RedisCommandArgument> | RedisCommandArgument;
selectors?: Array<Array<string>>;
} }
export function transformReply(reply: AclGetUserRawReply): AclUser { export function transformReply(reply: AclGetUserRawReply): AclUser {
@@ -31,6 +34,7 @@ export function transformReply(reply: AclGetUserRawReply): AclUser {
passwords: reply[3], passwords: reply[3],
commands: reply[5], commands: reply[5],
keys: reply[7], keys: reply[7],
channels: reply[9] channels: reply[9],
selectors: reply[11]
}; };
} }

View File

@@ -22,7 +22,7 @@ export function transformArguments(
range.end.toString() range.end.toString()
); );
if (range?.mode) { if (range.mode) {
args.push(range.mode); args.push(range.mode);
} }
} }

View File

@@ -9,7 +9,11 @@ export function assertPingCommand(commandInfo: CommandReply | null | undefined):
{ {
name: 'ping', name: 'ping',
arity: -1, arity: -1,
flags: new Set([CommandFlags.STALE, CommandFlags.FAST]), flags: new Set(
testUtils.isVersionGreaterThan([7]) ?
[CommandFlags.FAST] :
[CommandFlags.STALE, CommandFlags.FAST]
),
firstKeyIndex: 0, firstKeyIndex: 0,
lastKeyIndex: 0, lastKeyIndex: 0,
step: 0, step: 0,

View File

@@ -434,6 +434,26 @@ describe('AGGREGATE', () => {
); );
}); });
}); });
it('with PARAMS', () => {
assert.deepEqual(
transformArguments('index', '*', {
PARAMS: {
param: 'value'
}
}),
['FT.AGGREGATE', 'index', '*', 'PARAMS', '2', 'param', 'value']
);
});
it('with DIALECT', () => {
assert.deepEqual(
transformArguments('index', '*', {
DIALECT: 1
}),
['FT.AGGREGATE', 'index', '*', 'DIALECT', '1']
);
});
}); });
testUtils.testWithClient('client.ft.aggregate', async client => { testUtils.testWithClient('client.ft.aggregate', async client => {

View File

@@ -1,6 +1,6 @@
import { RedisCommandArgument, RedisCommandArguments } from '@node-redis/client/dist/lib/commands'; import { RedisCommandArgument, RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { pushVerdictArgument, transformTuplesReply } from '@node-redis/client/dist/lib/commands/generic-transformers'; import { pushVerdictArgument, transformTuplesReply } from '@node-redis/client/dist/lib/commands/generic-transformers';
import { PropertyName, pushArgumentsWithLength, pushSortByArguments, SortByProperty } from '.'; import { Params, PropertyName, pushArgumentsWithLength, pushParamsArgs, pushSortByArguments, SortByProperty } from '.';
export enum AggregateSteps { export enum AggregateSteps {
GROUPBY = 'GROUPBY', GROUPBY = 'GROUPBY',
@@ -122,6 +122,8 @@ export interface AggregateOptions {
VERBATIM?: true; VERBATIM?: true;
LOAD?: LoadField | Array<LoadField>; LOAD?: LoadField | Array<LoadField>;
STEPS?: Array<GroupByStep | SortStep | ApplyStep | LimitStep | FilterStep>; STEPS?: Array<GroupByStep | SortStep | ApplyStep | LimitStep | FilterStep>;
PARAMS?: Params;
DIALECT?: number;
} }
export function transformArguments( export function transformArguments(
@@ -129,17 +131,16 @@ export function transformArguments(
query: string, query: string,
options?: AggregateOptions options?: AggregateOptions
): RedisCommandArguments { ): RedisCommandArguments {
return pushAggregatehOptions(
const args = ['FT.AGGREGATE', index, query]; ['FT.AGGREGATE', index, query],
pushAggregatehOptions(args, options); options
return args; );
} }
export function pushAggregatehOptions( export function pushAggregatehOptions(
args: RedisCommandArguments, args: RedisCommandArguments,
options?: AggregateOptions options?: AggregateOptions
): RedisCommandArguments { ): RedisCommandArguments {
if (options?.VERBATIM) { if (options?.VERBATIM) {
args.push('VERBATIM'); args.push('VERBATIM');
} }
@@ -202,6 +203,12 @@ export function pushAggregatehOptions(
} }
} }
pushParamsArgs(args, options?.PARAMS);
if (options?.DIALECT) {
args.push('DIALECT', options.DIALECT.toString());
}
return args; return args;
} }
@@ -257,7 +264,6 @@ function pushGroupByReducer(args: RedisCommandArguments, reducer: GroupByReducer
} }
} }
}); });
break; break;
} }

View File

@@ -1,7 +1,7 @@
import { strict as assert } from 'assert'; import { strict as assert } from 'assert';
import testUtils, { GLOBAL } from '../test-utils'; import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CREATE'; import { transformArguments } from './CREATE';
import { SchemaFieldTypes, SchemaTextFieldPhonetics, RedisSearchLanguages } from '.'; import { SchemaFieldTypes, SchemaTextFieldPhonetics, RedisSearchLanguages, VectorAlgorithms } from '.';
describe('CREATE', () => { describe('CREATE', () => {
describe('transformArguments', () => { describe('transformArguments', () => {
@@ -126,6 +126,52 @@ describe('CREATE', () => {
}); });
}); });
describe('VECTOR', () => {
it('Flat algorithm', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.VECTOR,
ALGORITHM: VectorAlgorithms.FLAT,
TYPE: 'FLOAT32',
DIM: 2,
DISTANCE_METRIC: 'L2',
INITIAL_CAP: 1000000,
BLOCK_SIZE: 1000
}
}),
[
'FT.CREATE', 'index', 'SCHEMA', 'field', 'VECTOR', 'FLAT', '10', 'TYPE',
'FLOAT32', 'DIM', '2', 'DISTANCE_METRIC', 'L2', 'INITIAL_CAP', '1000000',
'BLOCK_SIZE', '1000'
]
);
});
it('HNSW algorithm', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.VECTOR,
ALGORITHM: VectorAlgorithms.HNSW,
TYPE: 'FLOAT32',
DIM: 2,
DISTANCE_METRIC: 'L2',
INITIAL_CAP: 1000000,
M: 40,
EF_CONSTRUCTION: 250,
EF_RUNTIME: 20
}
}),
[
'FT.CREATE', 'index', 'SCHEMA', 'field', 'VECTOR', 'HNSW', '14', 'TYPE',
'FLOAT32', 'DIM', '2', 'DISTANCE_METRIC', 'L2', 'INITIAL_CAP', '1000000',
'M', '40', 'EF_CONSTRUCTION', '250', 'EF_RUNTIME', '20'
]
);
});
});
describe('with generic options', () => { describe('with generic options', () => {
it('with AS', () => { it('with AS', () => {
assert.deepEqual( assert.deepEqual(

View File

@@ -2,10 +2,32 @@ import { strict as assert } from 'assert';
import { transformArguments } from './EXPLAIN'; import { transformArguments } from './EXPLAIN';
describe('EXPLAIN', () => { describe('EXPLAIN', () => {
it('transformArguments', () => { describe('transformArguments', () => {
assert.deepEqual( it('simple', () => {
transformArguments('index', '*'), assert.deepEqual(
['FT.EXPLAIN', 'index', '*'] transformArguments('index', '*'),
); ['FT.EXPLAIN', 'index', '*']
);
});
it('with PARAMS', () => {
assert.deepEqual(
transformArguments('index', '*', {
PARAMS: {
param: 'value'
}
}),
['FT.EXPLAIN', 'index', '*', 'PARAMS', '2', 'param', 'value']
);
});
it('with DIALECT', () => {
assert.deepEqual(
transformArguments('index', '*', {
DIALECT: 1
}),
['FT.EXPLAIN', 'index', '*', 'DIALECT', '1']
);
});
}); });
}); });

View File

@@ -1,7 +1,26 @@
import { Params, pushParamsArgs } from ".";
export const IS_READ_ONLY = true; export const IS_READ_ONLY = true;
export function transformArguments(index: string, query: string): Array<string> { interface ExplainOptions {
return ['FT.EXPLAIN', index, query]; PARAMS?: Params;
DIALECT?: number;
}
export function transformArguments(
index: string,
query: string,
options?: ExplainOptions
): Array<string> {
const args = ['FT.EXPLAIN', index, query];
pushParamsArgs(args, options?.PARAMS);
if (options?.DIALECT) {
args.push('DIALECT', options.DIALECT.toString());
}
return args;
} }
export declare function transformReply(): string; export declare function transformReply(): string;

View File

@@ -15,32 +15,56 @@ describe('INFO', () => {
await client.ft.create('index', { await client.ft.create('index', {
field: SchemaFieldTypes.TEXT field: SchemaFieldTypes.TEXT
}); });
assert.deepEqual( assert.deepEqual(
await client.ft.info('index'), await client.ft.info('index'),
{ {
indexName: 'index', indexName: 'index',
indexOptions: [], indexOptions: [],
indexDefinition: { indexDefinition: Object.create(null, {
defaultScore: '1', default_score: {
keyType: 'HASH', value: '1',
prefixes: [''] configurable: true,
}, enumerable: true
attributes: [[ },
'identifier', key_type: {
'field', value: 'HASH',
'attribute', configurable: true,
'field', enumerable: true
'type', },
'TEXT', prefixes: {
'WEIGHT', value: [''],
'1' configurable: true,
]], enumerable: true
}
}),
attributes: [Object.create(null, {
identifier: {
value: 'field',
configurable: true,
enumerable: true
},
attribute: {
value: 'field',
configurable: true,
enumerable: true
},
type: {
value: 'TEXT',
configurable: true,
enumerable: true
},
WEIGHT: {
value: '1',
configurable: true,
enumerable: true
}
})],
numDocs: '0', numDocs: '0',
maxDocId: '0', maxDocId: '0',
numTerms: '0', numTerms: '0',
numRecords: '0', numRecords: '0',
invertedSzMb: '0', invertedSzMb: '0',
vectorIndexSzMb: '0',
totalInvertedIndexBlocks: '0', totalInvertedIndexBlocks: '0',
offsetVectorsSzMb: '0', offsetVectorsSzMb: '0',
docTableSizeMb: '0', docTableSizeMb: '0',
@@ -67,7 +91,8 @@ describe('INFO', () => {
globalTotal: 0, globalTotal: 0,
indexCapacity: 128, indexCapacity: 128,
idnexTotal: 0 idnexTotal: 0
} },
stopWords: undefined
} }
); );
}, GLOBAL.SERVERS.OPEN); }, GLOBAL.SERVERS.OPEN);

View File

@@ -1,121 +1,118 @@
import { RedisCommandArgument } from '@node-redis/client/dist/lib/commands';
import { transformTuplesReply } from '@node-redis/client/dist/lib/commands/generic-transformers';
export function transformArguments(index: string): Array<string> { export function transformArguments(index: string): Array<string> {
return ['FT.INFO', index]; return ['FT.INFO', index];
} }
type InfoRawReply = [ type InfoRawReply = [
_: string, 'index_name',
indexName: string, RedisCommandArgument,
_: string, 'index_options',
indexOptions: Array<string>, Array<RedisCommandArgument>,
_: string, 'index_definition',
indexDefinition: [ Array<RedisCommandArgument>,
_: string, 'attributes',
keyType: string, Array<Array<RedisCommandArgument>>,
_: string, 'num_docs',
prefixes: Array<string>, RedisCommandArgument,
_: string, 'max_doc_id',
defaultScore: string RedisCommandArgument,
'num_terms',
RedisCommandArgument,
'num_records',
RedisCommandArgument,
'inverted_sz_mb',
RedisCommandArgument,
'vector_index_sz_mb',
RedisCommandArgument,
'total_inverted_index_blocks',
RedisCommandArgument,
'offset_vectors_sz_mb',
RedisCommandArgument,
'doc_table_size_mb',
RedisCommandArgument,
'sortable_values_size_mb',
RedisCommandArgument,
'key_table_size_mb',
RedisCommandArgument,
'records_per_doc_avg',
RedisCommandArgument,
'bytes_per_record_avg',
RedisCommandArgument,
'offsets_per_term_avg',
RedisCommandArgument,
'offset_bits_per_record_avg',
RedisCommandArgument,
'hash_indexing_failures',
RedisCommandArgument,
'indexing',
RedisCommandArgument,
'percent_indexed',
RedisCommandArgument,
'gc_stats',
[
'bytes_collected',
RedisCommandArgument,
'total_ms_run',
RedisCommandArgument,
'total_cycles',
RedisCommandArgument,
'average_cycle_time_ms',
RedisCommandArgument,
'last_run_time_ms',
RedisCommandArgument,
'gc_numeric_trees_missed',
RedisCommandArgument,
'gc_blocks_denied',
RedisCommandArgument
], ],
_: string, 'cursor_stats',
attributes: Array<Array<string>>, [
_: string, 'global_idle',
numDocs: string, number,
_: string, 'global_total',
maxDocId: string, number,
_: string, 'index_capacity',
numTerms: string, number,
_: string, 'index_total',
numRecords: string, number
_: string,
invertedSzMb: string,
_: string,
totalInvertedIndexBlocks: string,
_: string,
offsetVectorsSzMb: string,
_: string,
docTableSizeMb: string,
_: string,
sortableValuesSizeMb: string,
_: string,
keyTableSizeMb: string,
_: string,
recordsPerDocAvg: string,
_: string,
bytesPerRecordAvg: string,
_: string,
offsetsPerTermAvg: string,
_: string,
offsetBitsPerRecordAvg: string,
_: string,
hashIndexingFailures: string,
_: string,
indexing: string,
_: string,
percentIndexed: string,
_: string,
gcStats: [
_: string,
bytesCollected: string,
_: string,
totalMsRun: string,
_: string,
totalCycles: string,
_: string,
averageCycleTimeMs: string,
_: string,
lastRunTimeMs: string,
_: string,
gcNumericTreesMissed: string,
_: string,
gcBlocksDenied: string
], ],
_: string, 'stopwords_list'?,
cursorStats: [ Array<RedisCommandArgument>?
_: string,
globalIdle: number,
_: string,
globalTotal: number,
_: string,
indexCapacity: number,
_: string,
idnexTotal: number
]
]; ];
interface InfoReply { interface InfoReply {
indexName: string; indexName: RedisCommandArgument;
indexOptions: Array<string>; indexOptions: Array<RedisCommandArgument>;
indexDefinition: { indexDefinition: Record<string, RedisCommandArgument>;
keyType: string; attributes: Array<Record<string, RedisCommandArgument>>;
prefixes: Array<string>; numDocs: RedisCommandArgument;
defaultScore: string; maxDocId: RedisCommandArgument;
}; numTerms: RedisCommandArgument;
attributes: Array<Array<string>>; numRecords: RedisCommandArgument;
numDocs: string; invertedSzMb: RedisCommandArgument;
maxDocId: string; vectorIndexSzMb: RedisCommandArgument;
numTerms: string; totalInvertedIndexBlocks: RedisCommandArgument;
numRecords: string; offsetVectorsSzMb: RedisCommandArgument;
invertedSzMb: string; docTableSizeMb: RedisCommandArgument;
totalInvertedIndexBlocks: string; sortableValuesSizeMb: RedisCommandArgument;
offsetVectorsSzMb: string; keyTableSizeMb: RedisCommandArgument;
docTableSizeMb: string; recordsPerDocAvg: RedisCommandArgument;
sortableValuesSizeMb: string; bytesPerRecordAvg: RedisCommandArgument;
keyTableSizeMb: string; offsetsPerTermAvg: RedisCommandArgument;
recordsPerDocAvg: string; offsetBitsPerRecordAvg: RedisCommandArgument;
bytesPerRecordAvg: string; hashIndexingFailures: RedisCommandArgument;
offsetsPerTermAvg: string; indexing: RedisCommandArgument;
offsetBitsPerRecordAvg: string; percentIndexed: RedisCommandArgument;
hashIndexingFailures: string;
indexing: string;
percentIndexed: string;
gcStats: { gcStats: {
bytesCollected: string; bytesCollected: RedisCommandArgument;
totalMsRun: string; totalMsRun: RedisCommandArgument;
totalCycles: string; totalCycles: RedisCommandArgument;
averageCycleTimeMs: string; averageCycleTimeMs: RedisCommandArgument;
lastRunTimeMs: string; lastRunTimeMs: RedisCommandArgument;
gcNumericTreesMissed: string; gcNumericTreesMissed: RedisCommandArgument;
gcBlocksDenied: string; gcBlocksDenied: RedisCommandArgument;
}; };
cursorStats: { cursorStats: {
globalIdle: number; globalIdle: number;
@@ -123,49 +120,49 @@ interface InfoReply {
indexCapacity: number; indexCapacity: number;
idnexTotal: number; idnexTotal: number;
}; };
stopWords: Array<RedisCommandArgument> | undefined;
} }
export function transformReply(rawReply: InfoRawReply): InfoReply { export function transformReply(rawReply: InfoRawReply): InfoReply {
console.log(rawReply);
return { return {
indexName: rawReply[1], indexName: rawReply[1],
indexOptions: rawReply[3], indexOptions: rawReply[3],
indexDefinition: { indexDefinition: transformTuplesReply(rawReply[5]),
keyType: rawReply[5][1], attributes: rawReply[7].map(attribute => transformTuplesReply(attribute)),
prefixes: rawReply[5][3],
defaultScore: rawReply[5][5]
},
attributes: rawReply[7],
numDocs: rawReply[9], numDocs: rawReply[9],
maxDocId: rawReply[11], maxDocId: rawReply[11],
numTerms: rawReply[13], numTerms: rawReply[13],
numRecords: rawReply[15], numRecords: rawReply[15],
invertedSzMb: rawReply[17], invertedSzMb: rawReply[17],
totalInvertedIndexBlocks: rawReply[19], vectorIndexSzMb: rawReply[19],
offsetVectorsSzMb: rawReply[21], totalInvertedIndexBlocks: rawReply[21],
docTableSizeMb: rawReply[23], offsetVectorsSzMb: rawReply[23],
sortableValuesSizeMb: rawReply[25], docTableSizeMb: rawReply[25],
keyTableSizeMb: rawReply[27], sortableValuesSizeMb: rawReply[27],
recordsPerDocAvg: rawReply[29], keyTableSizeMb: rawReply[29],
bytesPerRecordAvg: rawReply[31], recordsPerDocAvg: rawReply[31],
offsetsPerTermAvg: rawReply[33], bytesPerRecordAvg: rawReply[33],
offsetBitsPerRecordAvg: rawReply[35], offsetsPerTermAvg: rawReply[35],
hashIndexingFailures: rawReply[37], offsetBitsPerRecordAvg: rawReply[37],
indexing: rawReply[39], hashIndexingFailures: rawReply[39],
percentIndexed: rawReply[41], indexing: rawReply[41],
percentIndexed: rawReply[43],
gcStats: { gcStats: {
bytesCollected: rawReply[43][1], bytesCollected: rawReply[45][1],
totalMsRun: rawReply[43][3], totalMsRun: rawReply[45][3],
totalCycles: rawReply[43][5], totalCycles: rawReply[45][5],
averageCycleTimeMs: rawReply[43][7], averageCycleTimeMs: rawReply[45][7],
lastRunTimeMs: rawReply[43][9], lastRunTimeMs: rawReply[45][9],
gcNumericTreesMissed: rawReply[43][11], gcNumericTreesMissed: rawReply[45][11],
gcBlocksDenied: rawReply[43][13] gcBlocksDenied: rawReply[45][13]
}, },
cursorStats: { cursorStats: {
globalIdle: rawReply[45][1], globalIdle: rawReply[47][1],
globalTotal: rawReply[45][3], globalTotal: rawReply[47][3],
indexCapacity: rawReply[45][5], indexCapacity: rawReply[47][5],
idnexTotal: rawReply[45][7] idnexTotal: rawReply[47][7]
} },
stopWords: rawReply[49]
}; };
} }

View File

@@ -1,5 +1,6 @@
import { SearchOptions, SearchRawReply, transformReply as transformSearchReply } from './SEARCH'; import { SearchOptions, SearchRawReply, transformReply as transformSearchReply } from './SEARCH';
import { pushSearchOptions, ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.'; import { pushSearchOptions, ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.';
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
export const IS_READ_ONLY = true; export const IS_READ_ONLY = true;
@@ -7,7 +8,7 @@ export function transformArguments(
index: string, index: string,
query: string, query: string,
options?: ProfileOptions & SearchOptions options?: ProfileOptions & SearchOptions
): Array<string> { ): RedisCommandArguments {
const args = ['FT.PROFILE', index, 'SEARCH']; const args = ['FT.PROFILE', index, 'SEARCH'];
if (options?.LIMITED) { if (options?.LIMITED) {
@@ -15,8 +16,7 @@ export function transformArguments(
} }
args.push('QUERY', query); args.push('QUERY', query);
pushSearchOptions(args, options) return pushSearchOptions(args, options);
return args;
} }
type ProfileSearchRawReply = ProfileRawReply<SearchRawReply>; type ProfileSearchRawReply = ProfileRawReply<SearchRawReply>;

View File

@@ -213,31 +213,98 @@ describe('SEARCH', () => {
['FT.SEARCH', 'index', 'query', 'LIMIT', '0', '1'] ['FT.SEARCH', 'index', 'query', 'LIMIT', '0', '1']
); );
}); });
it('with PARAMS', () => {
assert.deepEqual(
transformArguments('index', 'query', {
PARAMS: {
param: 'value'
}
}),
['FT.SEARCH', 'index', 'query', 'PARAMS', '2', 'param', 'value']
);
});
it('with DIALECT', () => {
assert.deepEqual(
transformArguments('index', 'query', {
DIALECT: 1
}),
['FT.SEARCH', 'index', 'query', 'DIALECT', '1']
);
});
}); });
testUtils.testWithClient('client.ft.search', async client => { describe('client.ft.search', () => {
await Promise.all([ testUtils.testWithClient('DIALECT 1', async client => {
client.ft.create('index', { await Promise.all([
field: SchemaFieldTypes.NUMERIC client.ft.create('index', {
}), field: SchemaFieldTypes.NUMERIC
client.hSet('1', 'field', '1') }),
]); client.hSet('1', 'field', '1')
]);
assert.deepEqual( assert.deepEqual(
await client.ft.search('index', '*'), await client.ft.search('index', '*', {
{ DIALECT: 1
total: 1, }),
documents: [{ {
id: '1', total: 1,
value: Object.create(null, { documents: [{
field: { id: '1',
value: '1', value: Object.create(null, {
configurable: true, field: {
enumerable: true value: '1',
} configurable: true,
}) enumerable: true
}] }
} })
); }]
}, GLOBAL.SERVERS.OPEN); }
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('DIALECT 2', async client => {
await Promise.all([
client.ft.create('index', {
field: SchemaFieldTypes.NUMERIC
}),
client.hSet('1', 'field', '1'),
client.hSet('2', 'field', '2'),
client.hSet('3', 'field', '3')
]);
assert.deepEqual(
await client.ft.search('index', '@field:[$min $max]', {
PARAMS: {
min: 1,
max: 2
},
DIALECT: 2
}),
{
total: 2,
documents: [{
id: '1',
value: Object.create(null, {
field: {
value: '1',
configurable: true,
enumerable: true
}
})
}, {
id: '2',
value: Object.create(null, {
field: {
value: '2',
configurable: true,
enumerable: true
}
})
}]
}
);
}, GLOBAL.SERVERS.OPEN);
});
}); });

View File

@@ -1,6 +1,6 @@
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands'; import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { transformTuplesReply } from '@node-redis/client/dist/lib/commands/generic-transformers'; import { transformTuplesReply } from '@node-redis/client/dist/lib/commands/generic-transformers';
import { pushSearchOptions, RedisSearchLanguages, PropertyName, SortByProperty, SearchReply } from '.'; import { pushSearchOptions, RedisSearchLanguages, Params, PropertyName, SortByProperty, SearchReply } from '.';
export const FIRST_KEY_INDEX = 1; export const FIRST_KEY_INDEX = 1;
@@ -54,6 +54,8 @@ export interface SearchOptions {
from: number | string; from: number | string;
size: number | string; size: number | string;
}; };
PARAMS?: Params;
DIALECT?: number;
} }
export function transformArguments( export function transformArguments(
@@ -61,9 +63,10 @@ export function transformArguments(
query: string, query: string,
options?: SearchOptions options?: SearchOptions
): RedisCommandArguments { ): RedisCommandArguments {
const args: RedisCommandArguments = ['FT.SEARCH', index, query]; return pushSearchOptions(
pushSearchOptions(args, options); ['FT.SEARCH', index, query],
return args; options
);
} }
export type SearchRawReply = Array<any>; export type SearchRawReply = Array<any>;

View File

@@ -47,6 +47,15 @@ describe('SPELLCHECK', () => {
); );
}); });
}); });
it('with DIALECT', () => {
assert.deepEqual(
transformArguments('index', 'query', {
DIALECT: 1
}),
['FT.SPELLCHECK', 'index', 'query', 'DIALECT', '1']
);
});
}); });
testUtils.testWithClient('client.ft.spellCheck', async client => { testUtils.testWithClient('client.ft.spellCheck', async client => {

View File

@@ -6,6 +6,7 @@ interface SpellCheckTerms {
interface SpellCheckOptions { interface SpellCheckOptions {
DISTANCE?: number; DISTANCE?: number;
TERMS?: SpellCheckTerms | Array<SpellCheckTerms>; TERMS?: SpellCheckTerms | Array<SpellCheckTerms>;
DIALECT?: number;
} }
export function transformArguments(index: string, query: string, options?: SpellCheckOptions): Array<string> { export function transformArguments(index: string, query: string, options?: SpellCheckOptions): Array<string> {
@@ -25,6 +26,10 @@ export function transformArguments(index: string, query: string, options?: Spell
} }
} }
if (options?.DIALECT) {
args.push('DIALECT', options.DIALECT.toString());
}
return args; return args;
} }

View File

@@ -28,7 +28,7 @@ import * as SUGLEN from './SUGLEN';
import * as SYNDUMP from './SYNDUMP'; import * as SYNDUMP from './SYNDUMP';
import * as SYNUPDATE from './SYNUPDATE'; import * as SYNUPDATE from './SYNUPDATE';
import * as TAGVALS from './TAGVALS'; import * as TAGVALS from './TAGVALS';
import { RedisCommandArguments } from '@node-redis/client/dist/lib/commands'; import { RedisCommandArgument, RedisCommandArguments } from '@node-redis/client/dist/lib/commands';
import { pushOptionalVerdictArgument, pushVerdictArgument } from '@node-redis/client/dist/lib/commands/generic-transformers'; import { pushOptionalVerdictArgument, pushVerdictArgument } from '@node-redis/client/dist/lib/commands/generic-transformers';
import { SearchOptions } from './SEARCH'; import { SearchOptions } from './SEARCH';
@@ -172,16 +172,29 @@ export enum SchemaFieldTypes {
TEXT = 'TEXT', TEXT = 'TEXT',
NUMERIC = 'NUMERIC', NUMERIC = 'NUMERIC',
GEO = 'GEO', GEO = 'GEO',
TAG = 'TAG' TAG = 'TAG',
VECTOR = 'VECTOR'
} }
type CreateSchemaField<T extends SchemaFieldTypes, E = Record<keyof any, any>> = T | ({ type CreateSchemaField<
T extends SchemaFieldTypes,
E = Record<keyof any, any>
> = T | ({
type: T; type: T;
AS?: string; AS?: string;
SORTABLE?: true | 'UNF';
NOINDEX?: true;
} & E); } & E);
type CreateSchemaCommonField<
T extends SchemaFieldTypes,
E = Record<string, never>
> = CreateSchemaField<
T,
({
SORTABLE?: true | 'UNF';
NOINDEX?: true;
} & E)
>;
export enum SchemaTextFieldPhonetics { export enum SchemaTextFieldPhonetics {
DM_EN = 'dm:en', DM_EN = 'dm:en',
DM_FR = 'dm:fr', DM_FR = 'dm:fr',
@@ -189,27 +202,55 @@ export enum SchemaTextFieldPhonetics {
DM_ES = 'dm:es' DM_ES = 'dm:es'
} }
type CreateSchemaTextField = CreateSchemaField<SchemaFieldTypes.TEXT, { type CreateSchemaTextField = CreateSchemaCommonField<SchemaFieldTypes.TEXT, {
NOSTEM?: true; NOSTEM?: true;
WEIGHT?: number; WEIGHT?: number;
PHONETIC?: SchemaTextFieldPhonetics; PHONETIC?: SchemaTextFieldPhonetics;
}>; }>;
type CreateSchemaNumericField = CreateSchemaField<SchemaFieldTypes.NUMERIC>; type CreateSchemaNumericField = CreateSchemaCommonField<SchemaFieldTypes.NUMERIC>;
type CreateSchemaGeoField = CreateSchemaField<SchemaFieldTypes.GEO>; type CreateSchemaGeoField = CreateSchemaCommonField<SchemaFieldTypes.GEO>;
type CreateSchemaTagField = CreateSchemaField<SchemaFieldTypes.TAG, { type CreateSchemaTagField = CreateSchemaCommonField<SchemaFieldTypes.TAG, {
SEPARATOR?: string; SEPARATOR?: string;
CASESENSITIVE?: true; CASESENSITIVE?: true;
}>; }>;
export enum VectorAlgorithms {
FLAT = 'FLAT',
HNSW = 'HNSW'
}
type CreateSchemaVectorField<
T extends VectorAlgorithms,
A extends Record<string, unknown>
> = CreateSchemaField<SchemaFieldTypes.VECTOR, {
ALGORITHM: T;
TYPE: string;
DIM: number;
DISTANCE_METRIC: 'L2' | 'IP' | 'COSINE';
INITIAL_CAP?: number;
} & A>;
type CreateSchemaFlatVectorField = CreateSchemaVectorField<VectorAlgorithms.FLAT, {
BLOCK_SIZE?: number;
}>;
type CreateSchemaHNSWVectorField = CreateSchemaVectorField<VectorAlgorithms.HNSW, {
M?: number;
EF_CONSTRUCTION?: number;
EF_RUNTIME?: number;
}>;
export interface RediSearchSchema { export interface RediSearchSchema {
[field: string]: [field: string]:
CreateSchemaTextField | CreateSchemaTextField |
CreateSchemaNumericField | CreateSchemaNumericField |
CreateSchemaGeoField | CreateSchemaGeoField |
CreateSchemaTagField; CreateSchemaTagField |
CreateSchemaFlatVectorField |
CreateSchemaHNSWVectorField;
} }
export function pushSchema(args: RedisCommandArguments, schema: RediSearchSchema) { export function pushSchema(args: RedisCommandArguments, schema: RediSearchSchema) {
@@ -257,6 +298,47 @@ export function pushSchema(args: RedisCommandArguments, schema: RediSearchSchema
} }
break; break;
case SchemaFieldTypes.VECTOR:
args.push(fieldOptions.ALGORITHM);
pushArgumentsWithLength(args, () => {
args.push(
'TYPE', fieldOptions.TYPE,
'DIM', fieldOptions.DIM.toString(),
'DISTANCE_METRIC', fieldOptions.DISTANCE_METRIC
);
if (fieldOptions.INITIAL_CAP) {
args.push('INITIAL_CAP', fieldOptions.INITIAL_CAP.toString());
}
switch (fieldOptions.ALGORITHM) {
case VectorAlgorithms.FLAT:
if (fieldOptions.BLOCK_SIZE) {
args.push('BLOCK_SIZE', fieldOptions.BLOCK_SIZE.toString());
}
break;
case VectorAlgorithms.HNSW:
if (fieldOptions.M) {
args.push('M', fieldOptions.M.toString());
}
if (fieldOptions.EF_CONSTRUCTION) {
args.push('EF_CONSTRUCTION', fieldOptions.EF_CONSTRUCTION.toString());
}
if (fieldOptions.EF_RUNTIME) {
args.push('EF_RUNTIME', fieldOptions.EF_RUNTIME.toString());
}
break;
}
});
continue; // vector fields do not contain SORTABLE and NOINDEX options
} }
if (fieldOptions.SORTABLE) { if (fieldOptions.SORTABLE) {
@@ -273,11 +355,27 @@ export function pushSchema(args: RedisCommandArguments, schema: RediSearchSchema
} }
} }
export type Params = Record<string, RedisCommandArgument | number>;
export function pushParamsArgs(
args: RedisCommandArguments,
params?: Params
): RedisCommandArguments {
if (params) {
const enrties = Object.entries(params);
args.push('PARAMS', (enrties.length * 2).toString());
for (const [key, value] of enrties) {
args.push(key, value.toString());
}
}
return args;
}
export function pushSearchOptions( export function pushSearchOptions(
args: RedisCommandArguments, args: RedisCommandArguments,
options?: SearchOptions options?: SearchOptions
): RedisCommandArguments { ): RedisCommandArguments {
if (options?.VERBATIM) { if (options?.VERBATIM) {
args.push('VERBATIM'); args.push('VERBATIM');
} }
@@ -381,6 +479,16 @@ export function pushSearchOptions(
); );
} }
if (options?.PARAMS) {
pushParamsArgs(args, options.PARAMS);
}
if (options?.DIALECT) {
args.push('DIALECT', options.DIALECT.toString());
}
console.log('!@#', args);
return args; return args;
} }

View File

@@ -1,5 +1,5 @@
export { default } from './commands'; export { default } from './commands';
export { RediSearchSchema, SchemaFieldTypes, SchemaTextFieldPhonetics, SearchReply } from './commands'; export { RediSearchSchema, SchemaFieldTypes, SchemaTextFieldPhonetics, SearchReply, VectorAlgorithms } from './commands';
export { AggregateSteps, AggregateGroupByReducers } from './commands/AGGREGATE'; export { AggregateSteps, AggregateGroupByReducers } from './commands/AGGREGATE';
export { SearchOptions } from './commands/SEARCH'; export { SearchOptions } from './commands/SEARCH';

View File

@@ -4,7 +4,7 @@ import RediSearch from '.';
export default new TestUtils({ export default new TestUtils({
dockerImageName: 'redislabs/redisearch', dockerImageName: 'redislabs/redisearch',
dockerImageVersionArgument: 'redisearch-version', dockerImageVersionArgument: 'redisearch-version',
defaultDockerVersion: '2.2.7' defaultDockerVersion: '2.4.3'
}); });
export const GLOBAL = { export const GLOBAL = {

View File

@@ -38,7 +38,7 @@ const portIterator = (async function*(): AsyncIterableIterator<number> {
export interface RedisServerDockerConfig { export interface RedisServerDockerConfig {
image: string; image: string;
version: Array<number>; version: string;
} }
export interface RedisServerDocker { export interface RedisServerDocker {
@@ -54,7 +54,7 @@ async function spawnRedisServerDocker({ image, version }: RedisServerDockerConfi
{ stdout, stderr } = await execAsync( { stdout, stderr } = await execAsync(
'docker run -d --network host $(' + 'docker run -d --network host $(' +
`docker build ${DOCKER_FODLER_PATH} -q ` + `docker build ${DOCKER_FODLER_PATH} -q ` +
`--build-arg IMAGE=${image}:${version.join('.')} ` + `--build-arg IMAGE=${image}:${version} ` +
`--build-arg REDIS_ARGUMENTS="--save --port ${port.toString()} ${serverArguments.join(' ')}"` + `--build-arg REDIS_ARGUMENTS="--save --port ${port.toString()} ${serverArguments.join(' ')}"` +
')' ')'
); );

View File

@@ -27,49 +27,61 @@ interface ClusterTestOptions<M extends RedisModules, S extends RedisScripts> ext
numberOfNodes?: number; numberOfNodes?: number;
} }
interface Version {
string: string;
numbers: Array<number>;
}
export default class TestUtils { export default class TestUtils {
static #getVersion(argumentName: string, defaultVersion: string): Array<number> { static #getVersion(argumentName: string, defaultVersion: string): Version {
return yargs(hideBin(process.argv)) return yargs(hideBin(process.argv))
.option(argumentName, { .option(argumentName, {
type: 'string', type: 'string',
default: defaultVersion default: defaultVersion
}) })
.coerce(argumentName, (arg: string) => { .coerce(argumentName, (arg: string) => {
return arg.split('.').map(x => { const indexOfDash = arg.indexOf('-');
const value = Number(x); return {
if (Number.isNaN(value)) { string: arg,
throw new TypeError(`${arg} is not a valid redis version`); numbers: (indexOfDash === -1 ? arg : arg.substring(0, indexOfDash)).split('.').map(x => {
} const value = Number(x);
if (Number.isNaN(value)) {
throw new TypeError(`${arg} is not a valid redis version`);
}
return value; return value;
}); })
};
}) })
.demandOption(argumentName) .demandOption(argumentName)
.parseSync()[argumentName]; .parseSync()[argumentName];
} }
readonly #VERSION_NUMBERS: Array<number>;
readonly #DOCKER_IMAGE: RedisServerDockerConfig; readonly #DOCKER_IMAGE: RedisServerDockerConfig;
constructor(config: TestUtilsConfig) { constructor(config: TestUtilsConfig) {
const { string, numbers } = TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion);
this.#VERSION_NUMBERS = numbers;
this.#DOCKER_IMAGE = { this.#DOCKER_IMAGE = {
image: config.dockerImageName, image: config.dockerImageName,
version: TestUtils.#getVersion(config.dockerImageVersionArgument, config.defaultDockerVersion) version: string
}; };
} }
isVersionGreaterThan(minimumVersion: Array<number> | undefined): boolean { isVersionGreaterThan(minimumVersion: Array<number> | undefined): boolean {
if (minimumVersion === undefined) return true; if (minimumVersion === undefined) return true;
const lastIndex = Math.min(this.#DOCKER_IMAGE.version.length, minimumVersion.length) - 1; const lastIndex = Math.min(this.#VERSION_NUMBERS.length, minimumVersion.length) - 1;
for (let i = 0; i < lastIndex; i++) { for (let i = 0; i < lastIndex; i++) {
if (this.#DOCKER_IMAGE.version[i] > minimumVersion[i]) { if (this.#VERSION_NUMBERS[i] > minimumVersion[i]) {
return true; return true;
} else if (minimumVersion[i] > this.#DOCKER_IMAGE.version[i]) { } else if (minimumVersion[i] > this.#VERSION_NUMBERS[i]) {
return false; return false;
} }
} }
return this.#DOCKER_IMAGE.version[lastIndex] >= minimumVersion[lastIndex]; return this.#VERSION_NUMBERS[lastIndex] >= minimumVersion[lastIndex];
} }
isVersionGreaterThanHook(minimumVersion: Array<number> | undefined): void { isVersionGreaterThanHook(minimumVersion: Array<number> | undefined): void {