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

V5 bringing RESP3, Sentinel and TypeMapping to node-redis

RESP3 Support
   - Some commands responses in RESP3 aren't stable yet and therefore return an "untyped" ReplyUnion.
 
Sentinel

TypeMapping

Correctly types Multi commands

Note: some API changes to be further documented in v4-to-v5.md
This commit is contained in:
Shaya Potter
2024-10-15 17:46:52 +03:00
committed by GitHub
parent 2fc79bdfb3
commit b2d35c5286
1174 changed files with 45931 additions and 36274 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,316 +1,329 @@
import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { pushVerdictArgument, transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers';
import { Params, PropertyName, pushArgumentsWithLength, pushParamsArgs, pushSortByArguments, SortByProperty } from '.';
import { ArrayReply, BlobStringReply, Command, MapReply, NumberReply, RedisArgument, ReplyUnion, TypeMapping, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
import { RediSearchProperty } from './CREATE';
import { FtSearchParams, pushParamsArgument } from './SEARCH';
import { pushVariadicArgument, transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers';
export enum AggregateSteps {
GROUPBY = 'GROUPBY',
SORTBY = 'SORTBY',
APPLY = 'APPLY',
LIMIT = 'LIMIT',
FILTER = 'FILTER'
type LoadField = RediSearchProperty | {
identifier: RediSearchProperty;
AS?: RedisArgument;
}
interface AggregateStep<T extends AggregateSteps> {
type: T;
export const FT_AGGREGATE_STEPS = {
GROUPBY: 'GROUPBY',
SORTBY: 'SORTBY',
APPLY: 'APPLY',
LIMIT: 'LIMIT',
FILTER: 'FILTER'
} as const;
type FT_AGGREGATE_STEPS = typeof FT_AGGREGATE_STEPS;
export type FtAggregateStep = FT_AGGREGATE_STEPS[keyof FT_AGGREGATE_STEPS];
interface AggregateStep<T extends FtAggregateStep> {
type: T;
}
export enum AggregateGroupByReducers {
COUNT = 'COUNT',
COUNT_DISTINCT = 'COUNT_DISTINCT',
COUNT_DISTINCTISH = 'COUNT_DISTINCTISH',
SUM = 'SUM',
MIN = 'MIN',
MAX = 'MAX',
AVG = 'AVG',
STDDEV = 'STDDEV',
QUANTILE = 'QUANTILE',
TOLIST = 'TOLIST',
TO_LIST = 'TOLIST',
FIRST_VALUE = 'FIRST_VALUE',
RANDOM_SAMPLE = 'RANDOM_SAMPLE'
export const FT_AGGREGATE_GROUP_BY_REDUCERS = {
COUNT: 'COUNT',
COUNT_DISTINCT: 'COUNT_DISTINCT',
COUNT_DISTINCTISH: 'COUNT_DISTINCTISH',
SUM: 'SUM',
MIN: 'MIN',
MAX: 'MAX',
AVG: 'AVG',
STDDEV: 'STDDEV',
QUANTILE: 'QUANTILE',
TOLIST: 'TOLIST',
FIRST_VALUE: 'FIRST_VALUE',
RANDOM_SAMPLE: 'RANDOM_SAMPLE'
} as const;
type FT_AGGREGATE_GROUP_BY_REDUCERS = typeof FT_AGGREGATE_GROUP_BY_REDUCERS;
export type FtAggregateGroupByReducer = FT_AGGREGATE_GROUP_BY_REDUCERS[keyof FT_AGGREGATE_GROUP_BY_REDUCERS];
interface GroupByReducer<T extends FtAggregateGroupByReducer> {
type: T;
AS?: RedisArgument;
}
interface GroupByReducer<T extends AggregateGroupByReducers> {
type: T;
AS?: string;
interface GroupByReducerWithProperty<T extends FtAggregateGroupByReducer> extends GroupByReducer<T> {
property: RediSearchProperty;
}
type CountReducer = GroupByReducer<AggregateGroupByReducers.COUNT>;
type CountReducer = GroupByReducer<FT_AGGREGATE_GROUP_BY_REDUCERS['COUNT']>;
interface CountDistinctReducer extends GroupByReducer<AggregateGroupByReducers.COUNT_DISTINCT> {
property: PropertyName;
type CountDistinctReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['COUNT_DISTINCT']>;
type CountDistinctishReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['COUNT_DISTINCTISH']>;
type SumReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['SUM']>;
type MinReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['MIN']>;
type MaxReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['MAX']>;
type AvgReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['AVG']>;
type StdDevReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['STDDEV']>;
interface QuantileReducer extends GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['QUANTILE']> {
quantile: number;
}
interface CountDistinctishReducer extends GroupByReducer<AggregateGroupByReducers.COUNT_DISTINCTISH> {
property: PropertyName;
type ToListReducer = GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['TOLIST']>;
interface FirstValueReducer extends GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['FIRST_VALUE']> {
BY?: RediSearchProperty | {
property: RediSearchProperty;
direction?: 'ASC' | 'DESC';
};
}
interface SumReducer extends GroupByReducer<AggregateGroupByReducers.SUM> {
property: PropertyName;
}
interface MinReducer extends GroupByReducer<AggregateGroupByReducers.MIN> {
property: PropertyName;
}
interface MaxReducer extends GroupByReducer<AggregateGroupByReducers.MAX> {
property: PropertyName;
}
interface AvgReducer extends GroupByReducer<AggregateGroupByReducers.AVG> {
property: PropertyName;
}
interface StdDevReducer extends GroupByReducer<AggregateGroupByReducers.STDDEV> {
property: PropertyName;
}
interface QuantileReducer extends GroupByReducer<AggregateGroupByReducers.QUANTILE> {
property: PropertyName;
quantile: number;
}
interface ToListReducer extends GroupByReducer<AggregateGroupByReducers.TOLIST> {
property: PropertyName;
}
interface FirstValueReducer extends GroupByReducer<AggregateGroupByReducers.FIRST_VALUE> {
property: PropertyName;
BY?: PropertyName | {
property: PropertyName;
direction?: 'ASC' | 'DESC';
};
}
interface RandomSampleReducer extends GroupByReducer<AggregateGroupByReducers.RANDOM_SAMPLE> {
property: PropertyName;
sampleSize: number;
interface RandomSampleReducer extends GroupByReducerWithProperty<FT_AGGREGATE_GROUP_BY_REDUCERS['RANDOM_SAMPLE']> {
sampleSize: number;
}
type GroupByReducers = CountReducer | CountDistinctReducer | CountDistinctishReducer | SumReducer | MinReducer | MaxReducer | AvgReducer | StdDevReducer | QuantileReducer | ToListReducer | FirstValueReducer | RandomSampleReducer;
interface GroupByStep extends AggregateStep<AggregateSteps.GROUPBY> {
properties?: PropertyName | Array<PropertyName>;
REDUCE: GroupByReducers | Array<GroupByReducers>;
interface GroupByStep extends AggregateStep<FT_AGGREGATE_STEPS['GROUPBY']> {
properties?: RediSearchProperty | Array<RediSearchProperty>;
REDUCE: GroupByReducers | Array<GroupByReducers>;
}
interface SortStep extends AggregateStep<AggregateSteps.SORTBY> {
BY: SortByProperty | Array<SortByProperty>;
MAX?: number;
type SortByProperty = RedisArgument | {
BY: RediSearchProperty;
DIRECTION?: 'ASC' | 'DESC';
};
interface SortStep extends AggregateStep<FT_AGGREGATE_STEPS['SORTBY']> {
BY: SortByProperty | Array<SortByProperty>;
MAX?: number;
}
interface ApplyStep extends AggregateStep<AggregateSteps.APPLY> {
expression: string;
AS: string;
interface ApplyStep extends AggregateStep<FT_AGGREGATE_STEPS['APPLY']> {
expression: RedisArgument;
AS: RedisArgument;
}
interface LimitStep extends AggregateStep<AggregateSteps.LIMIT> {
from: number;
size: number;
interface LimitStep extends AggregateStep<FT_AGGREGATE_STEPS['LIMIT']> {
from: number;
size: number;
}
interface FilterStep extends AggregateStep<AggregateSteps.FILTER> {
expression: string;
interface FilterStep extends AggregateStep<FT_AGGREGATE_STEPS['FILTER']> {
expression: RedisArgument;
}
type LoadField = PropertyName | {
identifier: PropertyName;
AS?: string;
}
export interface AggregateOptions {
VERBATIM?: boolean;
ADDSCORES?: boolean;
LOAD?: LoadField | Array<LoadField>;
STEPS?: Array<GroupByStep | SortStep | ApplyStep | LimitStep | FilterStep>;
PARAMS?: Params;
DIALECT?: number;
TIMEOUT?: number;
}
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(
index: string,
query: string,
options?: AggregateOptions
): RedisCommandArguments {
return pushAggregatehOptions(
['FT.AGGREGATE', index, query],
options
);
}
export function pushAggregatehOptions(
args: RedisCommandArguments,
options?: AggregateOptions
): RedisCommandArguments {
if (options?.VERBATIM) {
args.push('VERBATIM');
}
if (options?.ADDSCORES) {
args.push('ADDSCORES');
}
if (options?.LOAD) {
args.push('LOAD');
pushArgumentsWithLength(args, () => {
if (Array.isArray(options.LOAD)) {
for (const load of options.LOAD) {
pushLoadField(args, load);
}
} else {
pushLoadField(args, options.LOAD!);
}
});
}
if (options?.STEPS) {
for (const step of options.STEPS) {
switch (step.type) {
case AggregateSteps.GROUPBY:
args.push('GROUPBY');
if (!step.properties) {
args.push('0');
} else {
pushVerdictArgument(args, step.properties);
}
if (Array.isArray(step.REDUCE)) {
for (const reducer of step.REDUCE) {
pushGroupByReducer(args, reducer);
}
} else {
pushGroupByReducer(args, step.REDUCE);
}
break;
case AggregateSteps.SORTBY:
pushSortByArguments(args, 'SORTBY', step.BY);
if (step.MAX) {
args.push('MAX', step.MAX.toString());
}
break;
case AggregateSteps.APPLY:
args.push('APPLY', step.expression, 'AS', step.AS);
break;
case AggregateSteps.LIMIT:
args.push('LIMIT', step.from.toString(), step.size.toString());
break;
case AggregateSteps.FILTER:
args.push('FILTER', step.expression);
break;
}
}
}
pushParamsArgs(args, options?.PARAMS);
if (options?.DIALECT) {
args.push('DIALECT', options.DIALECT.toString());
}
if (options?.TIMEOUT !== undefined) {
args.push('TIMEOUT', options.TIMEOUT.toString());
}
return args;
}
function pushLoadField(args: RedisCommandArguments, toLoad: LoadField): void {
if (typeof toLoad === 'string') {
args.push(toLoad);
} else {
args.push(toLoad.identifier);
if (toLoad.AS) {
args.push('AS', toLoad.AS);
}
}
}
function pushGroupByReducer(args: RedisCommandArguments, reducer: GroupByReducers): void {
args.push('REDUCE', reducer.type);
switch (reducer.type) {
case AggregateGroupByReducers.COUNT:
args.push('0');
break;
case AggregateGroupByReducers.COUNT_DISTINCT:
case AggregateGroupByReducers.COUNT_DISTINCTISH:
case AggregateGroupByReducers.SUM:
case AggregateGroupByReducers.MIN:
case AggregateGroupByReducers.MAX:
case AggregateGroupByReducers.AVG:
case AggregateGroupByReducers.STDDEV:
case AggregateGroupByReducers.TOLIST:
args.push('1', reducer.property);
break;
case AggregateGroupByReducers.QUANTILE:
args.push('2', reducer.property, reducer.quantile.toString());
break;
case AggregateGroupByReducers.FIRST_VALUE: {
pushArgumentsWithLength(args, () => {
args.push(reducer.property);
if (reducer.BY) {
args.push('BY');
if (typeof reducer.BY === 'string') {
args.push(reducer.BY);
} else {
args.push(reducer.BY.property);
if (reducer.BY.direction) {
args.push(reducer.BY.direction);
}
}
}
});
break;
}
case AggregateGroupByReducers.RANDOM_SAMPLE:
args.push('2', reducer.property, reducer.sampleSize.toString());
break;
}
if (reducer.AS) {
args.push('AS', reducer.AS);
}
export interface FtAggregateOptions {
VERBATIM?: boolean;
ADDSCORES?: boolean;
LOAD?: LoadField | Array<LoadField>;
TIMEOUT?: number;
STEPS?: Array<GroupByStep | SortStep | ApplyStep | LimitStep | FilterStep>;
PARAMS?: FtSearchParams;
DIALECT?: number;
}
export type AggregateRawReply = [
total: number,
...results: Array<Array<RedisCommandArgument>>
total: UnwrapReply<NumberReply>,
...results: UnwrapReply<ArrayReply<ArrayReply<BlobStringReply>>>
];
export interface AggregateReply {
total: number;
results: Array<Record<string, RedisCommandArgument>>;
}
total: number;
results: Array<MapReply<BlobStringReply, BlobStringReply>>;
};
export function transformReply(rawReply: AggregateRawReply): AggregateReply {
const results: Array<Record<string, RedisCommandArgument>> = [];
for (let i = 1; i < rawReply.length; i++) {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: false,
transformArguments(index: RedisArgument, query: RedisArgument, options?: FtAggregateOptions) {
const args = ['FT.AGGREGATE', index, query];
return pushAggregateOptions(args, options);
},
transformReply: {
2: (rawReply: AggregateRawReply, preserve?: any, typeMapping?: TypeMapping): AggregateReply => {
const results: Array<MapReply<BlobStringReply, BlobStringReply>> = [];
for (let i = 1; i < rawReply.length; i++) {
results.push(
transformTuplesReply(rawReply[i] as Array<RedisCommandArgument>)
transformTuplesReply(rawReply[i] as ArrayReply<BlobStringReply>, preserve, typeMapping)
);
}
return {
total: Number(rawReply[0]),
results
};
},
3: undefined as unknown as () => ReplyUnion
},
unstableResp3: true
} as const satisfies Command;
export function pushAggregateOptions(args: Array<RedisArgument>, options?: FtAggregateOptions) {
if (options?.VERBATIM) {
args.push('VERBATIM');
}
if (options?.ADDSCORES) {
args.push('ADDSCORES');
}
if (options?.LOAD) {
const length = args.push('LOAD', '');
if (Array.isArray(options.LOAD)) {
for (const load of options.LOAD) {
pushLoadField(args, load);
}
} else {
pushLoadField(args, options.LOAD);
}
return {
total: rawReply[0],
results
};
args[length - 1] = (args.length - length).toString();
}
if (options?.TIMEOUT !== undefined) {
args.push('TIMEOUT', options.TIMEOUT.toString());
}
if (options?.STEPS) {
for (const step of options.STEPS) {
args.push(step.type);
switch (step.type) {
case FT_AGGREGATE_STEPS.GROUPBY:
if (!step.properties) {
args.push('0');
} else {
pushVariadicArgument(args, step.properties);
}
if (Array.isArray(step.REDUCE)) {
for (const reducer of step.REDUCE) {
pushGroupByReducer(args, reducer);
}
} else {
pushGroupByReducer(args, step.REDUCE);
}
break;
case FT_AGGREGATE_STEPS.SORTBY:
const length = args.push('');
if (Array.isArray(step.BY)) {
for (const by of step.BY) {
pushSortByProperty(args, by);
}
} else {
pushSortByProperty(args, step.BY);
}
if (step.MAX) {
args.push('MAX', step.MAX.toString());
}
args[length - 1] = (args.length - length).toString();
break;
case FT_AGGREGATE_STEPS.APPLY:
args.push(step.expression, 'AS', step.AS);
break;
case FT_AGGREGATE_STEPS.LIMIT:
args.push(step.from.toString(), step.size.toString());
break;
case FT_AGGREGATE_STEPS.FILTER:
args.push(step.expression);
break;
}
}
}
pushParamsArgument(args, options?.PARAMS);
if (options?.DIALECT !== undefined) {
args.push('DIALECT', options.DIALECT.toString());
}
return args;
}
function pushLoadField(args: Array<RedisArgument>, toLoad: LoadField) {
if (typeof toLoad === 'string' || toLoad instanceof Buffer) {
args.push(toLoad);
} else {
args.push(toLoad.identifier);
if (toLoad.AS) {
args.push('AS', toLoad.AS);
}
}
}
function pushGroupByReducer(args: Array<RedisArgument>, reducer: GroupByReducers) {
args.push('REDUCE', reducer.type);
switch (reducer.type) {
case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT:
args.push('0');
break;
case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT_DISTINCT:
case FT_AGGREGATE_GROUP_BY_REDUCERS.COUNT_DISTINCTISH:
case FT_AGGREGATE_GROUP_BY_REDUCERS.SUM:
case FT_AGGREGATE_GROUP_BY_REDUCERS.MIN:
case FT_AGGREGATE_GROUP_BY_REDUCERS.MAX:
case FT_AGGREGATE_GROUP_BY_REDUCERS.AVG:
case FT_AGGREGATE_GROUP_BY_REDUCERS.STDDEV:
case FT_AGGREGATE_GROUP_BY_REDUCERS.TOLIST:
args.push('1', reducer.property);
break;
case FT_AGGREGATE_GROUP_BY_REDUCERS.QUANTILE:
args.push('2', reducer.property, reducer.quantile.toString());
break;
case FT_AGGREGATE_GROUP_BY_REDUCERS.FIRST_VALUE: {
const length = args.push('', reducer.property) - 1;
if (reducer.BY) {
args.push('BY');
if (typeof reducer.BY === 'string' || reducer.BY instanceof Buffer) {
args.push(reducer.BY);
} else {
args.push(reducer.BY.property);
if (reducer.BY.direction) {
args.push(reducer.BY.direction);
}
}
}
args[length - 1] = (args.length - length).toString();
break;
}
case FT_AGGREGATE_GROUP_BY_REDUCERS.RANDOM_SAMPLE:
args.push('2', reducer.property, reducer.sampleSize.toString());
break;
}
if (reducer.AS) {
args.push('AS', reducer.AS);
}
}
function pushSortByProperty(args: Array<RedisArgument>, sortBy: SortByProperty) {
if (typeof sortBy === 'string' || sortBy instanceof Buffer) {
args.push(sortBy);
} else {
args.push(sortBy.BY);
if (sortBy.DIRECTION) {
args.push(sortBy.DIRECTION);
}
}
}

View File

@@ -1,37 +1,47 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './AGGREGATE_WITHCURSOR';
import { SchemaFieldTypes } from '.';
import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR';
describe('AGGREGATE WITHCURSOR', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', '*'),
['FT.AGGREGATE', 'index', '*', 'WITHCURSOR']
);
});
it('with COUNT', () => {
assert.deepEqual(
transformArguments('index', '*', { COUNT: 1 }),
['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'COUNT', '1']
);
});
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
AGGREGATE_WITHCURSOR.transformArguments('index', '*'),
['FT.AGGREGATE', 'index', '*', 'WITHCURSOR']
);
});
testUtils.testWithClient('client.ft.aggregateWithCursor', async client => {
await client.ft.create('index', {
field: SchemaFieldTypes.NUMERIC
});
it('with COUNT', () => {
assert.deepEqual(
AGGREGATE_WITHCURSOR.transformArguments('index', '*', {
COUNT: 1
}),
['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'COUNT', '1']
);
});
assert.deepEqual(
await client.ft.aggregateWithCursor('index', '*'),
{
total: 0,
results: [],
cursor: 0
}
);
}, GLOBAL.SERVERS.OPEN);
it('with MAXIDLE', () => {
assert.deepEqual(
AGGREGATE_WITHCURSOR.transformArguments('index', '*', {
MAXIDLE: 1
}),
['FT.AGGREGATE', 'index', '*', 'WITHCURSOR', 'MAXIDLE', '1']
);
});
});
testUtils.testWithClient('client.ft.aggregateWithCursor', async client => {
await client.ft.create('index', {
field: 'NUMERIC'
});
assert.deepEqual(
await client.ft.aggregateWithCursor('index', '*'),
{
total: 0,
results: [],
cursor: 0
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,44 +1,47 @@
import {
AggregateOptions,
AggregateRawReply,
AggregateReply,
transformArguments as transformAggregateArguments,
transformReply as transformAggregateReply
} from './AGGREGATE';
import { RedisArgument, Command, ReplyUnion, NumberReply } from '@redis/client/dist/lib/RESP/types';
import AGGREGATE, { AggregateRawReply, AggregateReply, FtAggregateOptions } from './AGGREGATE';
export { FIRST_KEY_INDEX, IS_READ_ONLY } from './AGGREGATE';
interface AggregateWithCursorOptions extends AggregateOptions {
COUNT?: number;
export interface FtAggregateWithCursorOptions extends FtAggregateOptions {
COUNT?: number;
MAXIDLE?: number;
}
export function transformArguments(
index: string,
query: string,
options?: AggregateWithCursorOptions
) {
const args = transformAggregateArguments(index, query, options);
type AggregateWithCursorRawReply = [
result: AggregateRawReply,
cursor: NumberReply
];
export interface AggregateWithCursorReply extends AggregateReply {
cursor: NumberReply;
}
export default {
FIRST_KEY_INDEX: AGGREGATE.FIRST_KEY_INDEX,
IS_READ_ONLY: AGGREGATE.IS_READ_ONLY,
transformArguments(index: RedisArgument, query: RedisArgument, options?: FtAggregateWithCursorOptions) {
const args = AGGREGATE.transformArguments(index, query, options);
args.push('WITHCURSOR');
if (options?.COUNT) {
args.push('COUNT', options.COUNT.toString());
if (options?.COUNT !== undefined) {
args.push('COUNT', options.COUNT.toString());
}
if(options?.MAXIDLE !== undefined) {
args.push('MAXIDLE', options.MAXIDLE.toString());
}
return args;
}
type AggregateWithCursorRawReply = [
result: AggregateRawReply,
cursor: number
];
interface AggregateWithCursorReply extends AggregateReply {
cursor: number;
}
export function transformReply(reply: AggregateWithCursorRawReply): AggregateWithCursorReply {
return {
...transformAggregateReply(reply[0]),
},
transformReply: {
2: (reply: AggregateWithCursorRawReply): AggregateWithCursorReply => {
return {
...AGGREGATE.transformReply[2](reply[0]),
cursor: reply[1]
};
}
};
},
3: undefined as unknown as () => ReplyUnion
},
unstableResp3: true
} as const satisfies Command;

View File

@@ -1,11 +1,24 @@
import { strict as assert } from 'assert';
import { transformArguments } from './ALIASADD';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import ALIASADD from './ALIASADD';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('ALIASADD', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('alias', 'index'),
['FT.ALIASADD', 'alias', 'index']
);
});
describe('FT.ALIASADD', () => {
it('transformArguments', () => {
assert.deepEqual(
ALIASADD.transformArguments('alias', 'index'),
['FT.ALIASADD', 'alias', 'index']
);
});
testUtils.testWithClient('client.ft.aliasAdd', async client => {
const [, reply] = await Promise.all([
client.ft.create('index', {
field: SCHEMA_FIELD_TYPE.TEXT
}),
client.ft.aliasAdd('alias', 'index')
]);
assert.equal(reply, 'OK');
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,5 +1,10 @@
export function transformArguments(name: string, index: string): Array<string> {
return ['FT.ALIASADD', name, index];
}
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
export declare function transformReply(): 'OK';
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(alias: RedisArgument, index: RedisArgument) {
return ['FT.ALIASADD', alias, index];
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,11 +1,25 @@
import { strict as assert } from 'assert';
import { transformArguments } from './ALIASDEL';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import ALIASDEL from './ALIASDEL';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('ALIASDEL', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('alias', 'index'),
['FT.ALIASDEL', 'alias', 'index']
);
});
describe('FT.ALIASDEL', () => {
it('transformArguments', () => {
assert.deepEqual(
ALIASDEL.transformArguments('alias'),
['FT.ALIASDEL', 'alias']
);
});
testUtils.testWithClient('client.ft.aliasAdd', async client => {
const [, , reply] = await Promise.all([
client.ft.create('index', {
field: SCHEMA_FIELD_TYPE.TEXT
}),
client.ft.aliasAdd('alias', 'index'),
client.ft.aliasDel('alias')
]);
assert.equal(reply, 'OK');
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,5 +1,10 @@
export function transformArguments(name: string, index: string): Array<string> {
return ['FT.ALIASDEL', name, index];
}
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
export declare function transformReply(): 'OK';
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(alias: RedisArgument) {
return ['FT.ALIASDEL', alias];
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,11 +1,24 @@
import { strict as assert } from 'assert';
import { transformArguments } from './ALIASUPDATE';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import ALIASUPDATE from './ALIASUPDATE';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('ALIASUPDATE', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('alias', 'index'),
['FT.ALIASUPDATE', 'alias', 'index']
);
});
describe('FT.ALIASUPDATE', () => {
it('transformArguments', () => {
assert.deepEqual(
ALIASUPDATE.transformArguments('alias', 'index'),
['FT.ALIASUPDATE', 'alias', 'index']
);
});
testUtils.testWithClient('client.ft.aliasUpdate', async client => {
const [, reply] = await Promise.all([
client.ft.create('index', {
field: SCHEMA_FIELD_TYPE.TEXT
}),
client.ft.aliasUpdate('alias', 'index')
]);
assert.equal(reply, 'OK');
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,5 +1,10 @@
export function transformArguments(name: string, index: string): Array<string> {
return ['FT.ALIASUPDATE', name, index];
}
import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
export declare function transformReply(): 'OK';
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(alias: RedisArgument, index: RedisArgument) {
return ['FT.ALIASUPDATE', alias, index];
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,37 +1,35 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './ALTER';
import { SchemaFieldTypes } from '.';
import ALTER from './ALTER';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('ALTER', () => {
describe('transformArguments', () => {
it('with NOINDEX', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
NOINDEX: true,
SORTABLE: 'UNF',
AS: 'text'
}
}),
['FT.ALTER', 'index', 'SCHEMA', 'ADD', 'field', 'AS', 'text', 'TEXT', 'SORTABLE', 'UNF', 'NOINDEX']
);
});
describe('FT.ALTER', () => {
describe('transformArguments', () => {
it('with NOINDEX', () => {
assert.deepEqual(
ALTER.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TEXT,
NOINDEX: true,
SORTABLE: 'UNF',
AS: 'text'
}
}),
['FT.ALTER', 'index', 'SCHEMA', 'ADD', 'field', 'AS', 'text', 'TEXT', 'SORTABLE', 'UNF', 'NOINDEX']
);
});
});
testUtils.testWithClient('client.ft.create', async client => {
await Promise.all([
client.ft.create('index', {
title: SchemaFieldTypes.TEXT
}),
]);
testUtils.testWithClient('client.ft.create', async client => {
const [, reply] = await Promise.all([
client.ft.create('index', {
title: SCHEMA_FIELD_TYPE.TEXT
}),
client.ft.alter('index', {
body: SCHEMA_FIELD_TYPE.TEXT
})
]);
assert.equal(
await client.ft.alter('index', {
body: SchemaFieldTypes.TEXT
}),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
assert.equal(reply, 'OK');
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,10 +1,13 @@
import { RediSearchSchema, pushSchema } from '.';
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
import { RediSearchSchema, pushSchema } from './CREATE';
export function transformArguments(index: string, schema: RediSearchSchema): Array<string> {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(index: RedisArgument, schema: RediSearchSchema) {
const args = ['FT.ALTER', index, 'SCHEMA', 'ADD'];
pushSchema(args, schema);
return args;
}
export declare function transformReply(): 'OK';
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,25 +1,25 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CONFIG_GET';
import CONFIG_GET from './CONFIG_GET';
describe('CONFIG GET', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('TIMEOUT'),
['FT.CONFIG', 'GET', 'TIMEOUT']
);
});
describe('FT.CONFIG GET', () => {
it('transformArguments', () => {
assert.deepEqual(
CONFIG_GET.transformArguments('TIMEOUT'),
['FT.CONFIG', 'GET', 'TIMEOUT']
);
});
testUtils.testWithClient('client.ft.configGet', async client => {
assert.deepEqual(
await client.ft.configGet('TIMEOUT'),
Object.create(null, {
TIMEOUT: {
value: '500',
configurable: true,
enumerable: true
}
})
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('client.ft.configGet', async client => {
assert.deepEqual(
await client.ft.configGet('TIMEOUT'),
Object.create(null, {
TIMEOUT: {
value: '500',
configurable: true,
enumerable: true
}
})
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,16 +1,18 @@
export function transformArguments(option: string) {
import { ArrayReply, TuplesReply, BlobStringReply, NullReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types';
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(option: string) {
return ['FT.CONFIG', 'GET', option];
}
interface ConfigGetReply {
[option: string]: string | null;
}
export function transformReply(rawReply: Array<[string, string | null]>): ConfigGetReply {
const transformedReply: ConfigGetReply = Object.create(null);
for (const [key, value] of rawReply) {
transformedReply[key] = value;
},
transformReply(reply: UnwrapReply<ArrayReply<TuplesReply<[BlobStringReply, BlobStringReply | NullReply]>>>) {
const transformedReply: Record<string, BlobStringReply | NullReply> = Object.create(null);
for (const item of reply) {
const [key, value] = item as unknown as UnwrapReply<typeof item>;
transformedReply[key.toString()] = value;
}
return transformedReply;
}
}
} as const satisfies Command;

View File

@@ -1,12 +1,19 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CONFIG_SET';
import CONFIG_SET from './CONFIG_SET';
describe('CONFIG SET', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('TIMEOUT', '500'),
['FT.CONFIG', 'SET', 'TIMEOUT', '500']
);
});
describe('FT.CONFIG SET', () => {
it('transformArguments', () => {
assert.deepEqual(
CONFIG_SET.transformArguments('TIMEOUT', '500'),
['FT.CONFIG', 'SET', 'TIMEOUT', '500']
);
});
testUtils.testWithClient('client.ft.configSet', async client => {
assert.deepEqual(
await client.ft.configSet('TIMEOUT', '500'),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,5 +1,14 @@
export function transformArguments(option: string, value: string): Array<string> {
return ['FT.CONFIG', 'SET', option, value];
}
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
export declare function transformReply(): 'OK';
// using `string & {}` to avoid TS widening the type to `string`
// TODO
type FtConfigProperties = 'a' | 'b' | (string & {}) | Buffer;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(property: FtConfigProperties, value: RedisArgument) {
return ['FT.CONFIG', 'SET', property, value];
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,490 +1,475 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CREATE';
import { SchemaFieldTypes, SchemaTextFieldPhonetics, RedisSearchLanguages, VectorAlgorithms, SCHEMA_GEO_SHAPE_COORD_SYSTEM } from '.';
import CREATE, { SCHEMA_FIELD_TYPE, SCHEMA_TEXT_FIELD_PHONETIC, SCHEMA_VECTOR_FIELD_ALGORITHM, REDISEARCH_LANGUAGE } from './CREATE';
describe('CREATE', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
transformArguments('index', {}),
['FT.CREATE', 'index', 'SCHEMA']
);
});
describe('with fields', () => {
describe('TEXT', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', {
field: SchemaFieldTypes.TEXT
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT']
);
});
it('with NOSTEM', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
NOSTEM: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOSTEM']
);
});
it('with WEIGHT', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
WEIGHT: 1
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WEIGHT', '1']
);
});
it('with PHONETIC', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
PHONETIC: SchemaTextFieldPhonetics.DM_EN
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'PHONETIC', SchemaTextFieldPhonetics.DM_EN]
);
});
it('with WITHSUFFIXTRIE', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
WITHSUFFIXTRIE: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WITHSUFFIXTRIE']
);
});
it('with INDEXEMPTY', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
INDEXEMPTY: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'INDEXEMPTY']
);
});
});
it('NUMERIC', () => {
assert.deepEqual(
transformArguments('index', {
field: SchemaFieldTypes.NUMERIC
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'NUMERIC']
);
});
it('GEO', () => {
assert.deepEqual(
transformArguments('index', {
field: SchemaFieldTypes.GEO
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEO']
);
});
describe('TAG', () => {
describe('without options', () => {
it('SchemaFieldTypes.TAG', () => {
assert.deepEqual(
transformArguments('index', {
field: SchemaFieldTypes.TAG
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG']
);
});
it('{ type: SchemaFieldTypes.TAG }', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TAG
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG']
);
});
});
it('with SEPARATOR', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TAG,
SEPARATOR: 'separator'
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'SEPARATOR', 'separator']
);
});
it('with CASESENSITIVE', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TAG,
CASESENSITIVE: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'CASESENSITIVE']
);
});
it('with WITHSUFFIXTRIE', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TAG,
WITHSUFFIXTRIE: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'WITHSUFFIXTRIE']
);
});
it('with INDEXEMPTY', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TAG,
INDEXEMPTY: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'INDEXEMPTY']
);
});
});
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('GEOSHAPE', () => {
describe('without options', () => {
it('SCHEMA_FIELD_TYPE.GEOSHAPE', () => {
assert.deepEqual(
transformArguments('index', {
field: SchemaFieldTypes.GEOSHAPE
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE']
);
});
it('{ type: SCHEMA_FIELD_TYPE.GEOSHAPE }', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.GEOSHAPE
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE']
);
});
});
it('with COORD_SYSTEM', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.GEOSHAPE,
COORD_SYSTEM: SCHEMA_GEO_SHAPE_COORD_SYSTEM.SPHERICAL
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE', 'COORD_SYSTEM', 'SPHERICAL']
);
});
});
describe('with generic options', () => {
it('with AS', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
AS: 'as'
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'AS', 'as', 'TEXT']
);
});
describe('with SORTABLE', () => {
it('true', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
SORTABLE: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE']
);
});
it('UNF', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
SORTABLE: 'UNF'
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE', 'UNF']
);
});
});
it('with NOINDEX', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
NOINDEX: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOINDEX']
);
});
it('with INDEXMISSING', () => {
assert.deepEqual(
transformArguments('index', {
field: {
type: SchemaFieldTypes.TEXT,
INDEXMISSING: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'INDEXMISSING']
);
});
});
});
it('with ON', () => {
assert.deepEqual(
transformArguments('index', {}, {
ON: 'HASH'
}),
['FT.CREATE', 'index', 'ON', 'HASH', 'SCHEMA']
);
});
describe('with PREFIX', () => {
it('string', () => {
assert.deepEqual(
transformArguments('index', {}, {
PREFIX: 'prefix'
}),
['FT.CREATE', 'index', 'PREFIX', '1', 'prefix', 'SCHEMA']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('index', {}, {
PREFIX: ['1', '2']
}),
['FT.CREATE', 'index', 'PREFIX', '2', '1', '2', 'SCHEMA']
);
});
});
it('with FILTER', () => {
assert.deepEqual(
transformArguments('index', {}, {
FILTER: '@field != ""'
}),
['FT.CREATE', 'index', 'FILTER', '@field != ""', 'SCHEMA']
);
});
it('with LANGUAGE', () => {
assert.deepEqual(
transformArguments('index', {}, {
LANGUAGE: RedisSearchLanguages.ARABIC
}),
['FT.CREATE', 'index', 'LANGUAGE', RedisSearchLanguages.ARABIC, 'SCHEMA']
);
});
it('with LANGUAGE_FIELD', () => {
assert.deepEqual(
transformArguments('index', {}, {
LANGUAGE_FIELD: '@field'
}),
['FT.CREATE', 'index', 'LANGUAGE_FIELD', '@field', 'SCHEMA']
);
});
it('with SCORE', () => {
assert.deepEqual(
transformArguments('index', {}, {
SCORE: 1
}),
['FT.CREATE', 'index', 'SCORE', '1', 'SCHEMA']
);
});
it('with SCORE_FIELD', () => {
assert.deepEqual(
transformArguments('index', {}, {
SCORE_FIELD: '@field'
}),
['FT.CREATE', 'index', 'SCORE_FIELD', '@field', 'SCHEMA']
);
});
it('with MAXTEXTFIELDS', () => {
assert.deepEqual(
transformArguments('index', {}, {
MAXTEXTFIELDS: true
}),
['FT.CREATE', 'index', 'MAXTEXTFIELDS', 'SCHEMA']
);
});
it('with TEMPORARY', () => {
assert.deepEqual(
transformArguments('index', {}, {
TEMPORARY: 1
}),
['FT.CREATE', 'index', 'TEMPORARY', '1', 'SCHEMA']
);
});
it('with NOOFFSETS', () => {
assert.deepEqual(
transformArguments('index', {}, {
NOOFFSETS: true
}),
['FT.CREATE', 'index', 'NOOFFSETS', 'SCHEMA']
);
});
it('with NOHL', () => {
assert.deepEqual(
transformArguments('index', {}, {
NOHL: true
}),
['FT.CREATE', 'index', 'NOHL', 'SCHEMA']
);
});
it('with NOFIELDS', () => {
assert.deepEqual(
transformArguments('index', {}, {
NOFIELDS: true
}),
['FT.CREATE', 'index', 'NOFIELDS', 'SCHEMA']
);
});
it('with NOFREQS', () => {
assert.deepEqual(
transformArguments('index', {}, {
NOFREQS: true
}),
['FT.CREATE', 'index', 'NOFREQS', 'SCHEMA']
);
});
it('with SKIPINITIALSCAN', () => {
assert.deepEqual(
transformArguments('index', {}, {
SKIPINITIALSCAN: true
}),
['FT.CREATE', 'index', 'SKIPINITIALSCAN', 'SCHEMA']
);
});
describe('with STOPWORDS', () => {
it('string', () => {
assert.deepEqual(
transformArguments('index', {}, {
STOPWORDS: 'stopword'
}),
['FT.CREATE', 'index', 'STOPWORDS', '1', 'stopword', 'SCHEMA']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('index', {}, {
STOPWORDS: ['1', '2']
}),
['FT.CREATE', 'index', 'STOPWORDS', '2', '1', '2', 'SCHEMA']
);
});
});
describe('FT.CREATE', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}),
['FT.CREATE', 'index', 'SCHEMA']
);
});
testUtils.testWithClient('client.ft.create', async client => {
assert.equal(
await client.ft.create('index', {
field: SchemaFieldTypes.TEXT
describe('with fields', () => {
describe('TEXT', () => {
it('without options', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: SCHEMA_FIELD_TYPE.TEXT
}),
'OK'
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT']
);
});
it('with NOSTEM', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TEXT,
NOSTEM: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOSTEM']
);
});
it('with WEIGHT', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TEXT,
WEIGHT: 1
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WEIGHT', '1']
);
});
it('with PHONETIC', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TEXT,
PHONETIC: SCHEMA_TEXT_FIELD_PHONETIC.DM_EN
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'PHONETIC', SCHEMA_TEXT_FIELD_PHONETIC.DM_EN]
);
});
it('with WITHSUFFIXTRIE', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TEXT,
WITHSUFFIXTRIE: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'WITHSUFFIXTRIE']
);
});
});
it('NUMERIC', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: SCHEMA_FIELD_TYPE.NUMERIC
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'NUMERIC']
);
}, GLOBAL.SERVERS.OPEN);
});
it('GEO', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: SCHEMA_FIELD_TYPE.GEO
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEO']
);
});
describe('TAG', () => {
describe('without options', () => {
it('SCHEMA_FIELD_TYPE.TAG', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: SCHEMA_FIELD_TYPE.TAG
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG']
);
});
it('{ type: SCHEMA_FIELD_TYPE.TAG }', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TAG
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG']
);
});
});
it('with SEPARATOR', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TAG,
SEPARATOR: 'separator'
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'SEPARATOR', 'separator']
);
});
it('with CASESENSITIVE', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TAG,
CASESENSITIVE: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'CASESENSITIVE']
);
});
it('with WITHSUFFIXTRIE', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TAG,
WITHSUFFIXTRIE: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'WITHSUFFIXTRIE']
);
});
it('with INDEXEMPTY', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TAG,
INDEXEMPTY: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TAG', 'INDEXEMPTY']
);
});
});
describe('VECTOR', () => {
it('Flat algorithm', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.VECTOR,
ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.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(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.VECTOR,
ALGORITHM: SCHEMA_VECTOR_FIELD_ALGORITHM.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('GEOSHAPE', () => {
describe('without options', () => {
it('SCHEMA_FIELD_TYPE.GEOSHAPE', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: SCHEMA_FIELD_TYPE.GEOSHAPE
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE']
);
});
it('{ type: SCHEMA_FIELD_TYPE.GEOSHAPE }', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.GEOSHAPE
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE']
);
});
});
it('with COORD_SYSTEM', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.GEOSHAPE,
COORD_SYSTEM: 'SPHERICAL'
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'GEOSHAPE', 'COORD_SYSTEM', 'SPHERICAL']
);
});
});
it('with AS', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TEXT,
AS: 'as'
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'AS', 'as', 'TEXT']
);
});
describe('with SORTABLE', () => {
it('true', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TEXT,
SORTABLE: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE']
);
});
it('UNF', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TEXT,
SORTABLE: 'UNF'
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'SORTABLE', 'UNF']
);
});
});
it('with NOINDEX', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TEXT,
NOINDEX: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'NOINDEX']
);
});
it('with INDEXMISSING', () => {
assert.deepEqual(
CREATE.transformArguments('index', {
field: {
type: SCHEMA_FIELD_TYPE.TEXT,
INDEXMISSING: true
}
}),
['FT.CREATE', 'index', 'SCHEMA', 'field', 'TEXT', 'INDEXMISSING']
);
});
});
it('with ON', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
ON: 'HASH'
}),
['FT.CREATE', 'index', 'ON', 'HASH', 'SCHEMA']
);
});
describe('with PREFIX', () => {
it('string', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
PREFIX: 'prefix'
}),
['FT.CREATE', 'index', 'PREFIX', '1', 'prefix', 'SCHEMA']
);
});
it('Array', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
PREFIX: ['1', '2']
}),
['FT.CREATE', 'index', 'PREFIX', '2', '1', '2', 'SCHEMA']
);
});
});
it('with FILTER', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
FILTER: '@field != ""'
}),
['FT.CREATE', 'index', 'FILTER', '@field != ""', 'SCHEMA']
);
});
it('with LANGUAGE', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
LANGUAGE: REDISEARCH_LANGUAGE.ARABIC
}),
['FT.CREATE', 'index', 'LANGUAGE', REDISEARCH_LANGUAGE.ARABIC, 'SCHEMA']
);
});
it('with LANGUAGE_FIELD', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
LANGUAGE_FIELD: '@field'
}),
['FT.CREATE', 'index', 'LANGUAGE_FIELD', '@field', 'SCHEMA']
);
});
it('with SCORE', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
SCORE: 1
}),
['FT.CREATE', 'index', 'SCORE', '1', 'SCHEMA']
);
});
it('with SCORE_FIELD', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
SCORE_FIELD: '@field'
}),
['FT.CREATE', 'index', 'SCORE_FIELD', '@field', 'SCHEMA']
);
});
it('with MAXTEXTFIELDS', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
MAXTEXTFIELDS: true
}),
['FT.CREATE', 'index', 'MAXTEXTFIELDS', 'SCHEMA']
);
});
it('with TEMPORARY', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
TEMPORARY: 1
}),
['FT.CREATE', 'index', 'TEMPORARY', '1', 'SCHEMA']
);
});
it('with NOOFFSETS', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
NOOFFSETS: true
}),
['FT.CREATE', 'index', 'NOOFFSETS', 'SCHEMA']
);
});
it('with NOHL', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
NOHL: true
}),
['FT.CREATE', 'index', 'NOHL', 'SCHEMA']
);
});
it('with NOFIELDS', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
NOFIELDS: true
}),
['FT.CREATE', 'index', 'NOFIELDS', 'SCHEMA']
);
});
it('with NOFREQS', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
NOFREQS: true
}),
['FT.CREATE', 'index', 'NOFREQS', 'SCHEMA']
);
});
it('with SKIPINITIALSCAN', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
SKIPINITIALSCAN: true
}),
['FT.CREATE', 'index', 'SKIPINITIALSCAN', 'SCHEMA']
);
});
describe('with STOPWORDS', () => {
it('string', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
STOPWORDS: 'stopword'
}),
['FT.CREATE', 'index', 'STOPWORDS', '1', 'stopword', 'SCHEMA']
);
});
it('Array', () => {
assert.deepEqual(
CREATE.transformArguments('index', {}, {
STOPWORDS: ['1', '2']
}),
['FT.CREATE', 'index', 'STOPWORDS', '2', '1', '2', 'SCHEMA']
);
});
});
});
testUtils.testWithClient('client.ft.create', async client => {
assert.equal(
await client.ft.create('index', {
field: SCHEMA_FIELD_TYPE.TEXT
}),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,52 +1,323 @@
import { pushOptionalVerdictArgument } from '@redis/client/dist/lib/commands/generic-transformers';
import { RedisSearchLanguages, PropertyName, RediSearchSchema, pushSchema } from '.';
import { RedisArgument, SimpleStringReply, Command, CommandArguments } from '@redis/client/lib/RESP/types';
import { RedisVariadicArgument, pushOptionalVariadicArgument } from '@redis/client/lib/commands/generic-transformers';
interface CreateOptions {
ON?: 'HASH' | 'JSON';
PREFIX?: string | Array<string>;
FILTER?: string;
LANGUAGE?: RedisSearchLanguages;
LANGUAGE_FIELD?: PropertyName;
SCORE?: number;
SCORE_FIELD?: PropertyName;
// PAYLOAD_FIELD?: string;
MAXTEXTFIELDS?: true;
TEMPORARY?: number;
NOOFFSETS?: true;
NOHL?: true;
NOFIELDS?: true;
NOFREQS?: true;
SKIPINITIALSCAN?: true;
STOPWORDS?: string | Array<string>;
export const SCHEMA_FIELD_TYPE = {
TEXT: 'TEXT',
NUMERIC: 'NUMERIC',
GEO: 'GEO',
TAG: 'TAG',
VECTOR: 'VECTOR',
GEOSHAPE: 'GEOSHAPE'
} as const;
export type SchemaFieldType = typeof SCHEMA_FIELD_TYPE[keyof typeof SCHEMA_FIELD_TYPE];
interface SchemaField<T extends SchemaFieldType = SchemaFieldType> {
type: T;
AS?: RedisArgument;
INDEXMISSING?: boolean;
}
export function transformArguments(index: string, schema: RediSearchSchema, options?: CreateOptions): Array<string> {
interface SchemaCommonField<T extends SchemaFieldType = SchemaFieldType> extends SchemaField<T> {
SORTABLE?: boolean | 'UNF'
NOINDEX?: boolean;
}
export const SCHEMA_TEXT_FIELD_PHONETIC = {
DM_EN: 'dm:en',
DM_FR: 'dm:fr',
FM_PT: 'dm:pt',
DM_ES: 'dm:es'
} as const;
export type SchemaTextFieldPhonetic = typeof SCHEMA_TEXT_FIELD_PHONETIC[keyof typeof SCHEMA_TEXT_FIELD_PHONETIC];
interface SchemaTextField extends SchemaCommonField<typeof SCHEMA_FIELD_TYPE['TEXT']> {
NOSTEM?: boolean;
WEIGHT?: number;
PHONETIC?: SchemaTextFieldPhonetic;
WITHSUFFIXTRIE?: boolean;
INDEXEMPTY?: boolean;
}
interface SchemaNumericField extends SchemaCommonField<typeof SCHEMA_FIELD_TYPE['NUMERIC']> {}
interface SchemaGeoField extends SchemaCommonField<typeof SCHEMA_FIELD_TYPE['GEO']> {}
interface SchemaTagField extends SchemaCommonField<typeof SCHEMA_FIELD_TYPE['TAG']> {
SEPARATOR?: RedisArgument;
CASESENSITIVE?: boolean;
WITHSUFFIXTRIE?: boolean;
INDEXEMPTY?: boolean;
}
export const SCHEMA_VECTOR_FIELD_ALGORITHM = {
FLAT: 'FLAT',
HNSW: 'HNSW'
} as const;
export type SchemaVectorFieldAlgorithm = typeof SCHEMA_VECTOR_FIELD_ALGORITHM[keyof typeof SCHEMA_VECTOR_FIELD_ALGORITHM];
interface SchemaVectorField extends SchemaField<typeof SCHEMA_FIELD_TYPE['VECTOR']> {
ALGORITHM: SchemaVectorFieldAlgorithm;
TYPE: string;
DIM: number;
DISTANCE_METRIC: 'L2' | 'IP' | 'COSINE';
INITIAL_CAP?: number;
}
interface SchemaFlatVectorField extends SchemaVectorField {
ALGORITHM: typeof SCHEMA_VECTOR_FIELD_ALGORITHM['FLAT'];
BLOCK_SIZE?: number;
}
interface SchemaHNSWVectorField extends SchemaVectorField {
ALGORITHM: typeof SCHEMA_VECTOR_FIELD_ALGORITHM['HNSW'];
M?: number;
EF_CONSTRUCTION?: number;
EF_RUNTIME?: number;
}
export const SCHEMA_GEO_SHAPE_COORD_SYSTEM = {
SPHERICAL: 'SPHERICAL',
FLAT: 'FLAT'
} as const;
export type SchemaGeoShapeFieldCoordSystem = typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM[keyof typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM];
interface SchemaGeoShapeField extends SchemaField<typeof SCHEMA_FIELD_TYPE['GEOSHAPE']> {
COORD_SYSTEM?: SchemaGeoShapeFieldCoordSystem;
}
export interface RediSearchSchema {
[field: string]: (
SchemaTextField |
SchemaNumericField |
SchemaGeoField |
SchemaTagField |
SchemaFlatVectorField |
SchemaHNSWVectorField |
SchemaGeoShapeField |
SchemaFieldType
);
}
function pushCommonSchemaFieldOptions(args: CommandArguments, fieldOptions: SchemaCommonField) {
if (fieldOptions.SORTABLE) {
args.push('SORTABLE');
if (fieldOptions.SORTABLE === 'UNF') {
args.push('UNF');
}
}
if (fieldOptions.NOINDEX) {
args.push('NOINDEX');
}
}
export function pushSchema(args: CommandArguments, schema: RediSearchSchema) {
for (const [field, fieldOptions] of Object.entries(schema)) {
args.push(field);
if (typeof fieldOptions === 'string') {
args.push(fieldOptions);
continue;
}
if (fieldOptions.AS) {
args.push('AS', fieldOptions.AS);
}
args.push(fieldOptions.type);
if (fieldOptions.INDEXMISSING) {
args.push('INDEXMISSING');
}
switch (fieldOptions.type) {
case SCHEMA_FIELD_TYPE.TEXT:
if (fieldOptions.NOSTEM) {
args.push('NOSTEM');
}
if (fieldOptions.WEIGHT) {
args.push('WEIGHT', fieldOptions.WEIGHT.toString());
}
if (fieldOptions.PHONETIC) {
args.push('PHONETIC', fieldOptions.PHONETIC);
}
if (fieldOptions.WITHSUFFIXTRIE) {
args.push('WITHSUFFIXTRIE');
}
if (fieldOptions.INDEXEMPTY) {
args.push('INDEXEMPTY');
}
pushCommonSchemaFieldOptions(args, fieldOptions)
break;
case SCHEMA_FIELD_TYPE.NUMERIC:
case SCHEMA_FIELD_TYPE.GEO:
pushCommonSchemaFieldOptions(args, fieldOptions)
break;
case SCHEMA_FIELD_TYPE.TAG:
if (fieldOptions.SEPARATOR) {
args.push('SEPARATOR', fieldOptions.SEPARATOR);
}
if (fieldOptions.CASESENSITIVE) {
args.push('CASESENSITIVE');
}
if (fieldOptions.WITHSUFFIXTRIE) {
args.push('WITHSUFFIXTRIE');
}
if (fieldOptions.INDEXEMPTY) {
args.push('INDEXEMPTY');
}
pushCommonSchemaFieldOptions(args, fieldOptions)
break;
case SCHEMA_FIELD_TYPE.VECTOR:
args.push(fieldOptions.ALGORITHM);
const lengthIndex = args.push('') - 1;
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 SCHEMA_VECTOR_FIELD_ALGORITHM.FLAT:
if (fieldOptions.BLOCK_SIZE) {
args.push('BLOCK_SIZE', fieldOptions.BLOCK_SIZE.toString());
}
break;
case SCHEMA_VECTOR_FIELD_ALGORITHM.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;
}
args[lengthIndex] = (args.length - lengthIndex - 1).toString();
break;
case SCHEMA_FIELD_TYPE.GEOSHAPE:
if (fieldOptions.COORD_SYSTEM !== undefined) {
args.push('COORD_SYSTEM', fieldOptions.COORD_SYSTEM);
}
break;
}
}
}
export const REDISEARCH_LANGUAGE = {
ARABIC: 'Arabic',
BASQUE: 'Basque',
CATALANA: 'Catalan',
DANISH: 'Danish',
DUTCH: 'Dutch',
ENGLISH: 'English',
FINNISH: 'Finnish',
FRENCH: 'French',
GERMAN: 'German',
GREEK: 'Greek',
HUNGARIAN: 'Hungarian',
INDONESAIN: 'Indonesian',
IRISH: 'Irish',
ITALIAN: 'Italian',
LITHUANIAN: 'Lithuanian',
NEPALI: 'Nepali',
NORWEIGAN: 'Norwegian',
PORTUGUESE: 'Portuguese',
ROMANIAN: 'Romanian',
RUSSIAN: 'Russian',
SPANISH: 'Spanish',
SWEDISH: 'Swedish',
TAMIL: 'Tamil',
TURKISH: 'Turkish',
CHINESE: 'Chinese'
} as const;
export type RediSearchLanguage = typeof REDISEARCH_LANGUAGE[keyof typeof REDISEARCH_LANGUAGE];
export type RediSearchProperty = `${'@' | '$.'}${string}`;
export interface CreateOptions {
ON?: 'HASH' | 'JSON';
PREFIX?: RedisVariadicArgument;
FILTER?: RedisArgument;
LANGUAGE?: RediSearchLanguage;
LANGUAGE_FIELD?: RediSearchProperty;
SCORE?: number;
SCORE_FIELD?: RediSearchProperty;
// PAYLOAD_FIELD?: string;
MAXTEXTFIELDS?: boolean;
TEMPORARY?: number;
NOOFFSETS?: boolean;
NOHL?: boolean;
NOFIELDS?: boolean;
NOFREQS?: boolean;
SKIPINITIALSCAN?: boolean;
STOPWORDS?: RedisVariadicArgument;
}
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(index: RedisArgument, schema: RediSearchSchema, options?: CreateOptions) {
const args = ['FT.CREATE', index];
if (options?.ON) {
args.push('ON', options.ON);
args.push('ON', options.ON);
}
pushOptionalVerdictArgument(args, 'PREFIX', options?.PREFIX);
pushOptionalVariadicArgument(args, 'PREFIX', options?.PREFIX);
if (options?.FILTER) {
args.push('FILTER', options.FILTER);
args.push('FILTER', options.FILTER);
}
if (options?.LANGUAGE) {
args.push('LANGUAGE', options.LANGUAGE);
args.push('LANGUAGE', options.LANGUAGE);
}
if (options?.LANGUAGE_FIELD) {
args.push('LANGUAGE_FIELD', options.LANGUAGE_FIELD);
args.push('LANGUAGE_FIELD', options.LANGUAGE_FIELD);
}
if (options?.SCORE) {
args.push('SCORE', options.SCORE.toString());
args.push('SCORE', options.SCORE.toString());
}
if (options?.SCORE_FIELD) {
args.push('SCORE_FIELD', options.SCORE_FIELD);
args.push('SCORE_FIELD', options.SCORE_FIELD);
}
// if (options?.PAYLOAD_FIELD) {
@@ -54,38 +325,38 @@ export function transformArguments(index: string, schema: RediSearchSchema, opti
// }
if (options?.MAXTEXTFIELDS) {
args.push('MAXTEXTFIELDS');
args.push('MAXTEXTFIELDS');
}
if (options?.TEMPORARY) {
args.push('TEMPORARY', options.TEMPORARY.toString());
args.push('TEMPORARY', options.TEMPORARY.toString());
}
if (options?.NOOFFSETS) {
args.push('NOOFFSETS');
args.push('NOOFFSETS');
}
if (options?.NOHL) {
args.push('NOHL');
args.push('NOHL');
}
if (options?.NOFIELDS) {
args.push('NOFIELDS');
args.push('NOFIELDS');
}
if (options?.NOFREQS) {
args.push('NOFREQS');
args.push('NOFREQS');
}
if (options?.SKIPINITIALSCAN) {
args.push('SKIPINITIALSCAN');
args.push('SKIPINITIALSCAN');
}
pushOptionalVerdictArgument(args, 'STOPWORDS', options?.STOPWORDS);
pushOptionalVariadicArgument(args, 'STOPWORDS', options?.STOPWORDS);
args.push('SCHEMA');
pushSchema(args, schema);
return args;
}
export declare function transformReply(): 'OK';
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,33 +1,32 @@
import { strict as assert } from 'assert';
import { SchemaFieldTypes } from '.';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CURSOR_DEL';
import CURSOR_DEL from './CURSOR_DEL';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('CURSOR DEL', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('index', 0),
['FT.CURSOR', 'DEL', 'index', '0']
);
});
describe('FT.CURSOR DEL', () => {
it('transformArguments', () => {
assert.deepEqual(
CURSOR_DEL.transformArguments('index', 0),
['FT.CURSOR', 'DEL', 'index', '0']
);
});
testUtils.testWithClient('client.ft.cursorDel', async client => {
const [ ,, { cursor } ] = await Promise.all([
client.ft.create('idx', {
field: {
type: SchemaFieldTypes.TEXT
}
}),
client.hSet('key', 'field', 'value'),
client.ft.aggregateWithCursor('idx', '*', {
COUNT: 1
})
]);
testUtils.testWithClient('client.ft.cursorDel', async client => {
const [, , { cursor }] = await Promise.all([
client.ft.create('idx', {
field: {
type: SCHEMA_FIELD_TYPE.TEXT
}
}),
client.hSet('key', 'field', 'value'),
client.ft.aggregateWithCursor('idx', '*', {
COUNT: 1
})
]);
assert.equal(
await client.ft.cursorDel('idx', cursor),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
assert.equal(
await client.ft.cursorDel('idx', cursor),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,14 +1,10 @@
import { RedisCommandArgument } from '@redis/client/dist/lib/commands';
import { SimpleStringReply, Command, RedisArgument, NumberReply, UnwrapReply } from '@redis/client/dist/lib/RESP/types';
export const FIRST_KEY_INDEX = 1;
export function transformArguments(index: RedisCommandArgument, cursorId: number) {
return [
'FT.CURSOR',
'DEL',
index,
cursorId.toString()
];
}
export declare function transformReply(): 'OK';
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(index: RedisArgument, cursorId: UnwrapReply<NumberReply>) {
return ['FT.CURSOR', 'DEL', index, cursorId.toString()];
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,45 +1,44 @@
import { strict as assert } from 'assert';
import { SchemaFieldTypes } from '.';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './CURSOR_READ';
import CURSOR_READ from './CURSOR_READ';
describe('CURSOR READ', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', 0),
['FT.CURSOR', 'READ', 'index', '0']
);
});
it('with COUNT', () => {
assert.deepEqual(
transformArguments('index', 0, { COUNT: 1 }),
['FT.CURSOR', 'READ', 'index', '0', 'COUNT', '1']
);
});
describe('FT.CURSOR READ', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
CURSOR_READ.transformArguments('index', 0),
['FT.CURSOR', 'READ', 'index', '0']
);
});
testUtils.testWithClient('client.ft.cursorRead', async client => {
const [, , { cursor }] = await Promise.all([
client.ft.create('idx', {
field: {
type: SchemaFieldTypes.TEXT
}
}),
client.hSet('key', 'field', 'value'),
client.ft.aggregateWithCursor('idx', '*', {
COUNT: 1
})
]);
it('with COUNT', () => {
assert.deepEqual(
CURSOR_READ.transformArguments('index', 0, {
COUNT: 1
}),
['FT.CURSOR', 'READ', 'index', '0', 'COUNT', '1']
);
});
});
assert.deepEqual(
await client.ft.cursorRead('idx', cursor),
{
total: 0,
results: [],
cursor: 0
}
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('client.ft.cursorRead', async client => {
const [, , { cursor }] = await Promise.all([
client.ft.create('idx', {
field: 'TEXT'
}),
client.hSet('key', 'field', 'value'),
client.ft.aggregateWithCursor('idx', '*', {
COUNT: 1
})
]);
assert.deepEqual(
await client.ft.cursorRead('idx', cursor),
{
total: 0,
results: [],
cursor: 0
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,30 +1,22 @@
import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { RedisArgument, Command, UnwrapReply, NumberReply } from '@redis/client/dist/lib/RESP/types';
import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
interface CursorReadOptions {
COUNT?: number;
export interface FtCursorReadOptions {
COUNT?: number;
}
export function transformArguments(
index: RedisCommandArgument,
cursor: number,
options?: CursorReadOptions
): RedisCommandArguments {
const args = [
'FT.CURSOR',
'READ',
index,
cursor.toString()
];
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(index: RedisArgument, cursor: UnwrapReply<NumberReply>, options?: FtCursorReadOptions) {
const args = ['FT.CURSOR', 'READ', index, cursor.toString()];
if (options?.COUNT) {
args.push('COUNT', options.COUNT.toString());
if (options?.COUNT !== undefined) {
args.push('COUNT', options.COUNT.toString());
}
return args;
}
export { transformReply } from './AGGREGATE_WITHCURSOR';
},
transformReply: AGGREGATE_WITHCURSOR.transformReply,
unstableResp3: true
} as const satisfies Command;

View File

@@ -1,28 +1,28 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './DICTADD';
import DICTADD from './DICTADD';
describe('DICTADD', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('dictionary', 'term'),
['FT.DICTADD', 'dictionary', 'term']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('dictionary', ['1', '2']),
['FT.DICTADD', 'dictionary', '1', '2']
);
});
describe('FT.DICTADD', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
DICTADD.transformArguments('dictionary', 'term'),
['FT.DICTADD', 'dictionary', 'term']
);
});
testUtils.testWithClient('client.ft.dictAdd', async client => {
assert.equal(
await client.ft.dictAdd('dictionary', 'term'),
1
);
}, GLOBAL.SERVERS.OPEN);
it('Array', () => {
assert.deepEqual(
DICTADD.transformArguments('dictionary', ['1', '2']),
['FT.DICTADD', 'dictionary', '1', '2']
);
});
});
testUtils.testWithClient('client.ft.dictAdd', async client => {
assert.equal(
await client.ft.dictAdd('dictionary', 'term'),
1
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,8 +1,11 @@
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
import { pushVariadicArguments, RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
export function transformArguments(dictionary: string, term: string | Array<string>): RedisCommandArguments {
return pushVerdictArguments(['FT.DICTADD', dictionary], term);
}
export declare function transformReply(): number;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(dictionary: RedisArgument, term: RedisVariadicArgument) {
return pushVariadicArguments(['FT.DICTADD', dictionary], term);
},
transformReply: undefined as unknown as () => NumberReply
} as const satisfies Command;

View File

@@ -1,28 +1,28 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './DICTDEL';
import DICTDEL from './DICTDEL';
describe('DICTDEL', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
transformArguments('dictionary', 'term'),
['FT.DICTDEL', 'dictionary', 'term']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('dictionary', ['1', '2']),
['FT.DICTDEL', 'dictionary', '1', '2']
);
});
describe('FT.DICTDEL', () => {
describe('transformArguments', () => {
it('string', () => {
assert.deepEqual(
DICTDEL.transformArguments('dictionary', 'term'),
['FT.DICTDEL', 'dictionary', 'term']
);
});
testUtils.testWithClient('client.ft.dictDel', async client => {
assert.equal(
await client.ft.dictDel('dictionary', 'term'),
0
);
}, GLOBAL.SERVERS.OPEN);
it('Array', () => {
assert.deepEqual(
DICTDEL.transformArguments('dictionary', ['1', '2']),
['FT.DICTDEL', 'dictionary', '1', '2']
);
});
});
testUtils.testWithClient('client.ft.dictDel', async client => {
assert.equal(
await client.ft.dictDel('dictionary', 'term'),
0
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,8 +1,11 @@
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
import { pushVariadicArguments, RedisVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
export function transformArguments(dictionary: string, term: string | Array<string>): RedisCommandArguments {
return pushVerdictArguments(['FT.DICTDEL', dictionary], term);
}
export declare function transformReply(): number;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(dictionary: RedisArgument, term: RedisVariadicArgument) {
return pushVariadicArguments(['FT.DICTDEL', dictionary], term);
},
transformReply: undefined as unknown as () => NumberReply
} as const satisfies Command;

View File

@@ -1,21 +1,21 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './DICTDUMP';
import DICTDUMP from './DICTDUMP';
describe('DICTDUMP', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('dictionary'),
['FT.DICTDUMP', 'dictionary']
);
});
describe('FT.DICTDUMP', () => {
it('transformArguments', () => {
assert.deepEqual(
DICTDUMP.transformArguments('dictionary'),
['FT.DICTDUMP', 'dictionary']
);
});
testUtils.testWithClient('client.ft.dictDump', async client => {
await client.ft.dictAdd('dictionary', 'string')
testUtils.testWithClient('client.ft.dictDump', async client => {
const [, reply] = await Promise.all([
client.ft.dictAdd('dictionary', 'string'),
client.ft.dictDump('dictionary')
]);
assert.deepEqual(
await client.ft.dictDump('dictionary'),
['string']
);
}, GLOBAL.SERVERS.OPEN);
assert.deepEqual(reply, ['string']);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,5 +1,13 @@
export function transformArguments(dictionary: string): Array<string> {
return ['FT.DICTDUMP', dictionary];
}
import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
export declare function transformReply(): Array<string>;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(dictionary: RedisArgument) {
return ['FT.DICTDUMP', dictionary];
},
transformReply: {
2: undefined as unknown as () => ArrayReply<BlobStringReply>,
3: undefined as unknown as () => SetReply<BlobStringReply>
}
} as const satisfies Command;

View File

@@ -1,33 +1,33 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { SchemaFieldTypes } from '.';
import { transformArguments } from './DROPINDEX';
import DROPINDEX from './DROPINDEX';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('DROPINDEX', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index'),
['FT.DROPINDEX', 'index']
);
});
it('with DD', () => {
assert.deepEqual(
transformArguments('index', { DD: true }),
['FT.DROPINDEX', 'index', 'DD']
);
});
describe('FT.DROPINDEX', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
DROPINDEX.transformArguments('index'),
['FT.DROPINDEX', 'index']
);
});
testUtils.testWithClient('client.ft.dropIndex', async client => {
await client.ft.create('index', {
field: SchemaFieldTypes.TEXT
});
it('with DD', () => {
assert.deepEqual(
DROPINDEX.transformArguments('index', { DD: true }),
['FT.DROPINDEX', 'index', 'DD']
);
});
});
assert.equal(
await client.ft.dropIndex('index'),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('client.ft.dropIndex', async client => {
const [, reply] = await Promise.all([
client.ft.create('index', {
field: SCHEMA_FIELD_TYPE.TEXT
}),
client.ft.dropIndex('index')
]);
assert.equal(reply, 'OK');
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,15 +1,23 @@
interface DropIndexOptions {
DD?: true;
import { RedisArgument, SimpleStringReply, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
export interface FtDropIndexOptions {
DD?: true;
}
export function transformArguments(index: string, options?: DropIndexOptions): Array<string> {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(index: RedisArgument, options?: FtDropIndexOptions) {
const args = ['FT.DROPINDEX', index];
if (options?.DD) {
args.push('DD');
args.push('DD');
}
return args;
}
export declare function transformReply(): 'OK';
},
transformReply: {
2: undefined as unknown as () => SimpleStringReply<'OK'>,
3: undefined as unknown as () => NumberReply
}
} as const satisfies Command;

View File

@@ -1,33 +1,46 @@
import { strict as assert } from 'assert';
import { transformArguments } from './EXPLAIN';
import { strict as assert } from 'node:assert';
import EXPLAIN from './EXPLAIN';
import testUtils, { GLOBAL } from '../test-utils';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('EXPLAIN', () => {
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
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']
);
});
describe('transformArguments', () => {
it('simple', () => {
assert.deepEqual(
EXPLAIN.transformArguments('index', '*'),
['FT.EXPLAIN', 'index', '*']
);
});
it('with PARAMS', () => {
assert.deepEqual(
EXPLAIN.transformArguments('index', '*', {
PARAMS: {
param: 'value'
}
}),
['FT.EXPLAIN', 'index', '*', 'PARAMS', '2', 'param', 'value']
);
});
it('with DIALECT', () => {
assert.deepEqual(
EXPLAIN.transformArguments('index', '*', {
DIALECT: 1
}),
['FT.EXPLAIN', 'index', '*', 'DIALECT', '1']
);
});
});
testUtils.testWithClient('client.ft.dropIndex', async client => {
const [, reply] = await Promise.all([
client.ft.create('index', {
field: SCHEMA_FIELD_TYPE.TEXT
}),
client.ft.explain('index', '*')
]);
assert.equal(reply, '<WILDCARD>\n');
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,26 +1,28 @@
import { Params, pushParamsArgs } from ".";
import { RedisArgument, SimpleStringReply, Command } from '@redis/client/dist/lib/RESP/types';
import { FtSearchParams, pushParamsArgument } from './SEARCH';
export const IS_READ_ONLY = true;
interface ExplainOptions {
PARAMS?: Params;
DIALECT?: number;
export interface FtExplainOptions {
PARAMS?: FtSearchParams;
DIALECT?: number;
}
export function transformArguments(
index: string,
query: string,
options?: ExplainOptions
): Array<string> {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(
index: RedisArgument,
query: RedisArgument,
options?: FtExplainOptions
) {
const args = ['FT.EXPLAIN', index, query];
pushParamsArgs(args, options?.PARAMS);
pushParamsArgument(args, options?.PARAMS);
if (options?.DIALECT) {
args.push('DIALECT', options.DIALECT.toString());
args.push('DIALECT', options.DIALECT.toString());
}
return args;
}
export declare function transformReply(): string;
},
transformReply: undefined as unknown as () => SimpleStringReply
} as const satisfies Command;

View File

@@ -1,11 +1,11 @@
import { strict as assert } from 'assert';
import { transformArguments } from './EXPLAINCLI';
import { strict as assert } from 'node:assert';
import EXPLAINCLI from './EXPLAINCLI';
describe('EXPLAINCLI', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('index', '*'),
['FT.EXPLAINCLI', 'index', '*']
);
});
it('transformArguments', () => {
assert.deepEqual(
EXPLAINCLI.transformArguments('index', '*'),
['FT.EXPLAINCLI', 'index', '*']
);
});
});

View File

@@ -1,7 +1,10 @@
export const IS_READ_ONLY = true;
import { RedisArgument, ArrayReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
export function transformArguments(index: string, query: string): Array<string> {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(index: RedisArgument, query: RedisArgument) {
return ['FT.EXPLAINCLI', index, query];
}
export declare function transformReply(): Array<string>;
},
transformReply: undefined as unknown as () => ArrayReply<BlobStringReply>
} as const satisfies Command;

View File

@@ -1,26 +1,28 @@
import { strict as assert } from 'assert';
import { SchemaFieldTypes } from '.';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './INFO';
import INFO, { InfoReply } from './INFO';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('INFO', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('index'),
INFO.transformArguments('index'),
['FT.INFO', 'index']
);
});
testUtils.testWithClient('client.ft.info', async client => {
await client.ft.create('index', {
field: SchemaFieldTypes.TEXT
field: SCHEMA_FIELD_TYPE.TEXT
});
const ret = await client.ft.info('index');
// effectively testing that stopwords_list is not in ret
assert.deepEqual(
await client.ft.info('index'),
ret,
{
indexName: 'index',
indexOptions: [],
indexDefinition: Object.create(null, {
index_name: 'index',
index_options: [],
index_definition: Object.create(null, {
default_score: {
value: '1',
configurable: true,
@@ -59,41 +61,48 @@ describe('INFO', () => {
enumerable: true
}
})],
numDocs: '0',
maxDocId: '0',
numTerms: '0',
numRecords: '0',
invertedSzMb: '0',
vectorIndexSzMb: '0',
totalInvertedIndexBlocks: '0',
offsetVectorsSzMb: '0',
docTableSizeMb: '0',
sortableValuesSizeMb: '0',
keyTableSizeMb: '0',
recordsPerDocAvg: '-nan',
bytesPerRecordAvg: '-nan',
offsetsPerTermAvg: '-nan',
offsetBitsPerRecordAvg: '-nan',
hashIndexingFailures: '0',
indexing: '0',
percentIndexed: '1',
gcStats: {
bytesCollected: '0',
totalMsRun: '0',
totalCycles: '0',
averageCycleTimeMs: '-nan',
lastRunTimeMs: '0',
gcNumericTreesMissed: '0',
gcBlocksDenied: '0'
num_docs: 0,
max_doc_id: 0,
num_terms: 0,
num_records: 0,
inverted_sz_mb: 0,
vector_index_sz_mb: 0,
total_inverted_index_blocks: 0,
offset_vectors_sz_mb: 0,
doc_table_size_mb: 0,
sortable_values_size_mb: 0,
key_table_size_mb: 0,
records_per_doc_avg: NaN,
bytes_per_record_avg: NaN,
cleaning: 0,
offsets_per_term_avg: NaN,
offset_bits_per_record_avg: NaN,
geoshapes_sz_mb: 0,
hash_indexing_failures: 0,
indexing: 0,
percent_indexed: 1,
number_of_uses: 1,
tag_overhead_sz_mb: 0,
text_overhead_sz_mb: 0,
total_index_memory_sz_mb: 0,
total_indexing_time: 0,
gc_stats: {
bytes_collected: 0,
total_ms_run: 0,
total_cycles: 0,
average_cycle_time_ms: NaN,
last_run_time_ms: 0,
gc_numeric_trees_missed: 0,
gc_blocks_denied: 0
},
cursorStats: {
globalIdle: 0,
globalTotal: 0,
indexCapacity: 128,
idnexTotal: 0
cursor_stats: {
global_idle: 0,
global_total: 0,
index_capacity: 128,
index_total: 0
},
stopWords: undefined
}
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,167 +1,163 @@
import { RedisCommandArgument } from '@redis/client/dist/lib/commands';
import { transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers';
import { RedisArgument } from "@redis/client";
import { ArrayReply, BlobStringReply, Command, DoubleReply, MapReply, NullReply, NumberReply, ReplyUnion, SimpleStringReply, TypeMapping } from "@redis/client/dist/lib/RESP/types";
import { createTransformTuplesReplyFunc, transformDoubleReply } from "@redis/client/dist/lib/commands/generic-transformers";
import { TuplesReply } from '@redis/client/lib/RESP/types';
export function transformArguments(index: string): Array<string> {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(index: RedisArgument) {
return ['FT.INFO', index];
},
transformReply: {
2: transformV2Reply,
3: undefined as unknown as () => ReplyUnion
},
unstableResp3: true
} as const satisfies Command;
export interface InfoReply {
index_name: SimpleStringReply;
index_options: ArrayReply<SimpleStringReply>;
index_definition: MapReply<SimpleStringReply, SimpleStringReply>;
attributes: Array<MapReply<SimpleStringReply, SimpleStringReply>>;
num_docs: NumberReply
max_doc_id: NumberReply;
num_terms: NumberReply;
num_records: NumberReply;
inverted_sz_mb: DoubleReply;
vector_index_sz_mb: DoubleReply;
total_inverted_index_blocks: NumberReply;
offset_vectors_sz_mb: DoubleReply;
doc_table_size_mb: DoubleReply;
sortable_values_size_mb: DoubleReply;
key_table_size_mb: DoubleReply;
tag_overhead_sz_mb: DoubleReply;
text_overhead_sz_mb: DoubleReply;
total_index_memory_sz_mb: DoubleReply;
geoshapes_sz_mb: DoubleReply;
records_per_doc_avg: DoubleReply;
bytes_per_record_avg: DoubleReply;
offsets_per_term_avg: DoubleReply;
offset_bits_per_record_avg: DoubleReply;
hash_indexing_failures: NumberReply;
total_indexing_time: DoubleReply;
indexing: NumberReply;
percent_indexed: DoubleReply;
number_of_uses: NumberReply;
cleaning: NumberReply;
gc_stats: {
bytes_collected: DoubleReply;
total_ms_run: DoubleReply;
total_cycles: DoubleReply;
average_cycle_time_ms: DoubleReply;
last_run_time_ms: DoubleReply;
gc_numeric_trees_missed: DoubleReply;
gc_blocks_denied: DoubleReply;
};
cursor_stats: {
global_idle: NumberReply;
global_total: NumberReply;
index_capacity: NumberReply;
index_total: NumberReply;
};
stopwords_list?: ArrayReply<BlobStringReply> | TuplesReply<[NullReply]>;
}
type InfoRawReply = [
'index_name',
RedisCommandArgument,
'index_options',
Array<RedisCommandArgument>,
'index_definition',
Array<RedisCommandArgument>,
'attributes',
Array<Array<RedisCommandArgument>>,
'num_docs',
RedisCommandArgument,
'max_doc_id',
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
],
'cursor_stats',
[
'global_idle',
number,
'global_total',
number,
'index_capacity',
number,
'index_total',
number
],
'stopwords_list'?,
Array<RedisCommandArgument>?
];
function transformV2Reply(reply: Array<any>, preserve?: any, typeMapping?: TypeMapping): InfoReply {
const myTransformFunc = createTransformTuplesReplyFunc<SimpleStringReply>(preserve, typeMapping);
interface InfoReply {
indexName: RedisCommandArgument;
indexOptions: Array<RedisCommandArgument>;
indexDefinition: Record<string, RedisCommandArgument>;
attributes: Array<Record<string, RedisCommandArgument>>;
numDocs: RedisCommandArgument;
maxDocId: RedisCommandArgument;
numTerms: RedisCommandArgument;
numRecords: RedisCommandArgument;
invertedSzMb: RedisCommandArgument;
vectorIndexSzMb: RedisCommandArgument;
totalInvertedIndexBlocks: RedisCommandArgument;
offsetVectorsSzMb: RedisCommandArgument;
docTableSizeMb: RedisCommandArgument;
sortableValuesSizeMb: RedisCommandArgument;
keyTableSizeMb: RedisCommandArgument;
recordsPerDocAvg: RedisCommandArgument;
bytesPerRecordAvg: RedisCommandArgument;
offsetsPerTermAvg: RedisCommandArgument;
offsetBitsPerRecordAvg: RedisCommandArgument;
hashIndexingFailures: RedisCommandArgument;
indexing: RedisCommandArgument;
percentIndexed: RedisCommandArgument;
gcStats: {
bytesCollected: RedisCommandArgument;
totalMsRun: RedisCommandArgument;
totalCycles: RedisCommandArgument;
averageCycleTimeMs: RedisCommandArgument;
lastRunTimeMs: RedisCommandArgument;
gcNumericTreesMissed: RedisCommandArgument;
gcBlocksDenied: RedisCommandArgument;
};
cursorStats: {
globalIdle: number;
globalTotal: number;
indexCapacity: number;
idnexTotal: number;
};
stopWords: Array<RedisCommandArgument> | undefined;
}
const ret = {} as unknown as InfoReply;
export function transformReply(rawReply: InfoRawReply): InfoReply {
return {
indexName: rawReply[1],
indexOptions: rawReply[3],
indexDefinition: transformTuplesReply(rawReply[5]),
attributes: rawReply[7].map(attribute => transformTuplesReply(attribute)),
numDocs: rawReply[9],
maxDocId: rawReply[11],
numTerms: rawReply[13],
numRecords: rawReply[15],
invertedSzMb: rawReply[17],
vectorIndexSzMb: rawReply[19],
totalInvertedIndexBlocks: rawReply[21],
offsetVectorsSzMb: rawReply[23],
docTableSizeMb: rawReply[25],
sortableValuesSizeMb: rawReply[27],
keyTableSizeMb: rawReply[29],
recordsPerDocAvg: rawReply[31],
bytesPerRecordAvg: rawReply[33],
offsetsPerTermAvg: rawReply[35],
offsetBitsPerRecordAvg: rawReply[37],
hashIndexingFailures: rawReply[39],
indexing: rawReply[41],
percentIndexed: rawReply[43],
gcStats: {
bytesCollected: rawReply[45][1],
totalMsRun: rawReply[45][3],
totalCycles: rawReply[45][5],
averageCycleTimeMs: rawReply[45][7],
lastRunTimeMs: rawReply[45][9],
gcNumericTreesMissed: rawReply[45][11],
gcBlocksDenied: rawReply[45][13]
},
cursorStats: {
globalIdle: rawReply[47][1],
globalTotal: rawReply[47][3],
indexCapacity: rawReply[47][5],
idnexTotal: rawReply[47][7]
},
stopWords: rawReply[49]
};
for (let i=0; i < reply.length; i += 2) {
const key = reply[i].toString() as keyof InfoReply;
switch (key) {
case 'index_name':
case 'index_options':
case 'num_docs':
case 'max_doc_id':
case 'num_terms':
case 'num_records':
case 'total_inverted_index_blocks':
case 'hash_indexing_failures':
case 'indexing':
case 'number_of_uses':
case 'cleaning':
case 'stopwords_list':
ret[key] = reply[i+1];
break;
case 'inverted_sz_mb':
case 'vector_index_sz_mb':
case 'offset_vectors_sz_mb':
case 'doc_table_size_mb':
case 'sortable_values_size_mb':
case 'key_table_size_mb':
case 'text_overhead_sz_mb':
case 'tag_overhead_sz_mb':
case 'total_index_memory_sz_mb':
case 'geoshapes_sz_mb':
case 'records_per_doc_avg':
case 'bytes_per_record_avg':
case 'offsets_per_term_avg':
case 'offset_bits_per_record_avg':
case 'total_indexing_time':
case 'percent_indexed':
ret[key] = transformDoubleReply[2](reply[i+1], undefined, typeMapping) as DoubleReply;
break;
case 'index_definition':
ret[key] = myTransformFunc(reply[i+1]);
break;
case 'attributes':
ret[key] = (reply[i+1] as Array<ArrayReply<SimpleStringReply>>).map(attribute => myTransformFunc(attribute));
break;
case 'gc_stats': {
const innerRet = {} as unknown as InfoReply['gc_stats'];
const array = reply[i+1];
for (let i=0; i < array.length; i += 2) {
const innerKey = array[i].toString() as keyof InfoReply['gc_stats'];
switch (innerKey) {
case 'bytes_collected':
case 'total_ms_run':
case 'total_cycles':
case 'average_cycle_time_ms':
case 'last_run_time_ms':
case 'gc_numeric_trees_missed':
case 'gc_blocks_denied':
innerRet[innerKey] = transformDoubleReply[2](array[i+1], undefined, typeMapping) as DoubleReply;
break;
}
}
ret[key] = innerRet;
break;
}
case 'cursor_stats': {
const innerRet = {} as unknown as InfoReply['cursor_stats'];
const array = reply[i+1];
for (let i=0; i < array.length; i += 2) {
const innerKey = array[i].toString() as keyof InfoReply['cursor_stats'];
switch (innerKey) {
case 'global_idle':
case 'global_total':
case 'index_capacity':
case 'index_total':
innerRet[innerKey] = array[i+1];
break;
}
}
ret[key] = innerRet;
break;
}
}
}
return ret;
}

View File

@@ -1,25 +1,25 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { SchemaFieldTypes } from '.';
import { transformArguments } from './PROFILE_AGGREGATE';
import { AggregateSteps } from './AGGREGATE';
import { FT_AGGREGATE_STEPS } from './AGGREGATE';
import PROFILE_AGGREGATE from './PROFILE_AGGREGATE';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('PROFILE AGGREGATE', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', 'query'),
PROFILE_AGGREGATE.transformArguments('index', 'query'),
['FT.PROFILE', 'index', 'AGGREGATE', 'QUERY', 'query']
);
});
it('with options', () => {
assert.deepEqual(
transformArguments('index', 'query', {
PROFILE_AGGREGATE.transformArguments('index', 'query', {
LIMITED: true,
VERBATIM: true,
STEPS: [{
type: AggregateSteps.SORTBY,
type: FT_AGGREGATE_STEPS.SORTBY,
BY: '@by'
}]
}),
@@ -32,13 +32,14 @@ describe('PROFILE AGGREGATE', () => {
testUtils.testWithClient('client.ft.search', async client => {
await Promise.all([
client.ft.create('index', {
field: SchemaFieldTypes.NUMERIC
field: SCHEMA_FIELD_TYPE.NUMERIC
}),
client.hSet('1', 'field', '1'),
client.hSet('2', 'field', '2')
]);
const res = await client.ft.profileAggregate('index', '*');
assert.deepEqual('None', res.profile.warning);
assert.ok(typeof res.profile.iteratorsProfile.counter === 'number');
assert.ok(typeof res.profile.parsingTime === 'string');
assert.ok(res.results.total == 1);

View File

@@ -1,29 +1,38 @@
import { pushAggregatehOptions, AggregateOptions, transformReply as transformAggregateReply, AggregateRawReply } from './AGGREGATE';
import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.';
// import { pushAggregatehOptions, AggregateOptions, transformReply as transformAggregateReply, AggregateRawReply } from './AGGREGATE';
// import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.';
export const IS_READ_ONLY = true;
import { Command, ReplyUnion } from "@redis/client/dist/lib/RESP/types";
import AGGREGATE, { AggregateRawReply, FtAggregateOptions, pushAggregateOptions } from "./AGGREGATE";
import { ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from "./PROFILE_SEARCH";
export function transformArguments(
index: string,
query: string,
options?: ProfileOptions & AggregateOptions
): Array<string> {
const args = ['FT.PROFILE', index, 'AGGREGATE'];
if (options?.LIMITED) {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(
index: string,
query: string,
options?: ProfileOptions & FtAggregateOptions
) {
const args = ['FT.PROFILE', index, 'AGGREGATE'];
if (options?.LIMITED) {
args.push('LIMITED');
}
}
args.push('QUERY', query);
args.push('QUERY', query);
pushAggregatehOptions(args, options)
return args;
}
return pushAggregateOptions(args, options)
},
transformReply: {
2: (reply: ProfileAggeregateRawReply): ProfileReply => {
return {
results: AGGREGATE.transformReply[2](reply[0]),
profile: transformProfile(reply[1])
}
},
3: undefined as unknown as () => ReplyUnion
},
unstableResp3: true
} as const satisfies Command;
type ProfileAggeregateRawReply = ProfileRawReply<AggregateRawReply>;
export function transformReply(reply: ProfileAggeregateRawReply): ProfileReply {
return {
results: transformAggregateReply(reply[0]),
profile: transformProfile(reply[1])
};
}
type ProfileAggeregateRawReply = ProfileRawReply<AggregateRawReply>;

View File

@@ -1,20 +1,21 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { SchemaFieldTypes } from '.';
import { transformArguments } from './PROFILE_SEARCH';
import PROFILE_SEARCH from './PROFILE_SEARCH';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('PROFILE SEARCH', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', 'query'),
PROFILE_SEARCH.transformArguments('index', 'query'),
['FT.PROFILE', 'index', 'SEARCH', 'QUERY', 'query']
);
});
it('with options', () => {
assert.deepEqual(
transformArguments('index', 'query', {
PROFILE_SEARCH.transformArguments('index', 'query', {
LIMITED: true,
VERBATIM: true,
INKEYS: 'key'
@@ -28,12 +29,13 @@ describe('PROFILE SEARCH', () => {
testUtils.testWithClient('client.ft.search', async client => {
await Promise.all([
client.ft.create('index', {
field: SchemaFieldTypes.NUMERIC
field: SCHEMA_FIELD_TYPE.NUMERIC
}),
client.hSet('1', 'field', '1')
]);
const res = await client.ft.profileSearch('index', '*');
assert.strictEqual('None', res.profile.warning);
assert.ok(typeof res.profile.iteratorsProfile.counter === 'number');
assert.ok(typeof res.profile.parsingTime === 'string');
assert.ok(res.results.total == 1);

View File

@@ -1,29 +1,152 @@
import { SearchOptions, SearchRawReply, transformReply as transformSearchReply } from './SEARCH';
import { pushSearchOptions, ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.';
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
// import { SearchOptions, SearchRawReply, transformReply as transformSearchReply } from './SEARCH';
// import { pushSearchOptions, ProfileOptions, ProfileRawReply, ProfileReply, transformProfile } from '.';
// import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
export const IS_READ_ONLY = true;
import { Command, RedisArgument, ReplyUnion } from "@redis/client/dist/lib/RESP/types";
import SEARCH, { FtSearchOptions, SearchRawReply, SearchReply, pushSearchOptions } from "./SEARCH";
import { AggregateReply } from "./AGGREGATE";
export function transformArguments(
index: string,
query: string,
options?: ProfileOptions & SearchOptions
): RedisCommandArguments {
let args: RedisCommandArguments = ['FT.PROFILE', index, 'SEARCH'];
if (options?.LIMITED) {
args.push('LIMITED');
}
args.push('QUERY', query);
return pushSearchOptions(args, options);
}
export type ProfileRawReply<T> = [
results: T,
profile: [
_: string,
TotalProfileTime: string,
_: string,
ParsingTime: string,
_: string,
PipelineCreationTime: string,
_: string,
IteratorsProfile: Array<any>
]
];
type ProfileSearchRawReply = ProfileRawReply<SearchRawReply>;
export function transformReply(reply: ProfileSearchRawReply, withoutDocuments: boolean): ProfileReply {
return {
results: transformSearchReply(reply[0], withoutDocuments),
profile: transformProfile(reply[1])
};
export interface ProfileOptions {
LIMITED?: true;
}
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(
index: RedisArgument,
query: RedisArgument,
options?: ProfileOptions & FtSearchOptions
) {
let args: Array<RedisArgument> = ['FT.PROFILE', index, 'SEARCH'];
if (options?.LIMITED) {
args.push('LIMITED');
}
args.push('QUERY', query);
return pushSearchOptions(args, options);
},
transformReply: {
2: (reply: ProfileSearchRawReply, withoutDocuments: boolean): ProfileReply => {
return {
results: SEARCH.transformReply[2](reply[0]),
profile: transformProfile(reply[1])
}
},
3: undefined as unknown as () => ReplyUnion
},
unstableResp3: true
} as const satisfies Command;
export interface ProfileReply {
results: SearchReply | AggregateReply;
profile: ProfileData;
}
interface ChildIterator {
type?: string,
counter?: number,
term?: string,
size?: number,
time?: string,
childIterators?: Array<ChildIterator>
}
interface IteratorsProfile {
type?: string,
counter?: number,
queryType?: string,
time?: string,
childIterators?: Array<ChildIterator>
}
interface ProfileData {
totalProfileTime: string,
parsingTime: string,
pipelineCreationTime: string,
warning: string,
iteratorsProfile: IteratorsProfile
}
export function transformProfile(reply: Array<any>): ProfileData{
return {
totalProfileTime: reply[0][1],
parsingTime: reply[1][1],
pipelineCreationTime: reply[2][1],
warning: reply[3][1] ? reply[3][1] : 'None',
iteratorsProfile: transformIterators(reply[4][1])
};
}
function transformIterators(IteratorsProfile: Array<any>): IteratorsProfile {
var res: IteratorsProfile = {};
for (let i = 0; i < IteratorsProfile.length; i += 2) {
const value = IteratorsProfile[i+1];
switch (IteratorsProfile[i]) {
case 'Type':
res.type = value;
break;
case 'Counter':
res.counter = value;
break;
case 'Time':
res.time = value;
break;
case 'Query type':
res.queryType = value;
break;
case 'Child iterators':
res.childIterators = value.map(transformChildIterators);
break;
}
}
return res;
}
function transformChildIterators(IteratorsProfile: Array<any>): ChildIterator {
var res: ChildIterator = {};
for (let i = 1; i < IteratorsProfile.length; i += 2) {
const value = IteratorsProfile[i+1];
switch (IteratorsProfile[i]) {
case 'Type':
res.type = value;
break;
case 'Counter':
res.counter = value;
break;
case 'Time':
res.time = value;
break;
case 'Size':
res.size = value;
break;
case 'Term':
res.term = value;
break;
case 'Child iterators':
res.childIterators = value.map(transformChildIterators);
break;
}
}
return res;
}

View File

@@ -1,300 +1,327 @@
import { strict as assert } from 'assert';
import { RedisSearchLanguages, SchemaFieldTypes } from '.';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SEARCH';
import SEARCH from './SEARCH';
describe('SEARCH', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', 'query'),
['FT.SEARCH', 'index', 'query']
);
});
it('with VERBATIM', () => {
assert.deepEqual(
transformArguments('index', 'query', { VERBATIM: true }),
['FT.SEARCH', 'index', 'query', 'VERBATIM']
);
});
it('with NOSTOPWORDS', () => {
assert.deepEqual(
transformArguments('index', 'query', { NOSTOPWORDS: true }),
['FT.SEARCH', 'index', 'query', 'NOSTOPWORDS']
);
});
it('with INKEYS', () => {
assert.deepEqual(
transformArguments('index', 'query', { INKEYS: 'key' }),
['FT.SEARCH', 'index', 'query', 'INKEYS', '1', 'key']
);
});
it('with INFIELDS', () => {
assert.deepEqual(
transformArguments('index', 'query', { INFIELDS: 'field' }),
['FT.SEARCH', 'index', 'query', 'INFIELDS', '1', 'field']
);
});
it('with RETURN', () => {
assert.deepEqual(
transformArguments('index', 'query', { RETURN: 'return' }),
['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return']
);
});
describe('with SUMMARIZE', () => {
it('true', () => {
assert.deepEqual(
transformArguments('index', 'query', { SUMMARIZE: true }),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE']
);
});
describe('with FIELDS', () => {
it('string', () => {
assert.deepEqual(
transformArguments('index', 'query', {
SUMMARIZE: {
FIELDS: ['@field']
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '1', '@field']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('index', 'query', {
SUMMARIZE: {
FIELDS: ['@1', '@2']
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '2', '@1', '@2']
);
});
});
it('with FRAGS', () => {
assert.deepEqual(
transformArguments('index', 'query', {
SUMMARIZE: {
FRAGS: 1
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FRAGS', '1']
);
});
it('with LEN', () => {
assert.deepEqual(
transformArguments('index', 'query', {
SUMMARIZE: {
LEN: 1
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'LEN', '1']
);
});
it('with SEPARATOR', () => {
assert.deepEqual(
transformArguments('index', 'query', {
SUMMARIZE: {
SEPARATOR: 'separator'
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'SEPARATOR', 'separator']
);
});
});
describe('with HIGHLIGHT', () => {
it('true', () => {
assert.deepEqual(
transformArguments('index', 'query', { HIGHLIGHT: true }),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT']
);
});
describe('with FIELDS', () => {
it('string', () => {
assert.deepEqual(
transformArguments('index', 'query', {
HIGHLIGHT: {
FIELDS: ['@field']
}
}),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '1', '@field']
);
});
it('Array', () => {
assert.deepEqual(
transformArguments('index', 'query', {
HIGHLIGHT: {
FIELDS: ['@1', '@2']
}
}),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '2', '@1', '@2']
);
});
});
it('with TAGS', () => {
assert.deepEqual(
transformArguments('index', 'query', {
HIGHLIGHT: {
TAGS: {
open: 'open',
close: 'close'
}
}
}),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'TAGS', 'open', 'close']
);
});
});
it('with SLOP', () => {
assert.deepEqual(
transformArguments('index', 'query', { SLOP: 1 }),
['FT.SEARCH', 'index', 'query', 'SLOP', '1']
);
});
it('with INORDER', () => {
assert.deepEqual(
transformArguments('index', 'query', { INORDER: true }),
['FT.SEARCH', 'index', 'query', 'INORDER']
);
});
it('with LANGUAGE', () => {
assert.deepEqual(
transformArguments('index', 'query', { LANGUAGE: RedisSearchLanguages.ARABIC }),
['FT.SEARCH', 'index', 'query', 'LANGUAGE', RedisSearchLanguages.ARABIC]
);
});
it('with EXPANDER', () => {
assert.deepEqual(
transformArguments('index', 'query', { EXPANDER: 'expender' }),
['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender']
);
});
it('with SCORER', () => {
assert.deepEqual(
transformArguments('index', 'query', { SCORER: 'scorer' }),
['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer']
);
});
it('with SORTBY', () => {
assert.deepEqual(
transformArguments('index', 'query', { SORTBY: '@by' }),
['FT.SEARCH', 'index', 'query', 'SORTBY', '@by']
);
});
it('with LIMIT', () => {
assert.deepEqual(
transformArguments('index', 'query', {
LIMIT: {
from: 0,
size: 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']
);
});
it('with TIMEOUT', () => {
assert.deepEqual(
transformArguments('index', 'query', {
TIMEOUT: 5
}),
['FT.SEARCH', 'index', 'query', 'TIMEOUT', '5']
);
});
describe('FT.SEARCH', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query'),
['FT.SEARCH', 'index', 'query']
);
});
describe('client.ft.search', () => {
testUtils.testWithClient('without optional options', async client => {
await Promise.all([
client.ft.create('index', {
field: SchemaFieldTypes.NUMERIC
}),
client.hSet('1', 'field', '1')
]);
assert.deepEqual(
await client.ft.search('index', '*'),
{
total: 1,
documents: [{
id: '1',
value: Object.create(null, {
field: {
value: '1',
configurable: true,
enumerable: true
}
})
}]
}
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('RETURN []', async client => {
await Promise.all([
client.ft.create('index', {
field: SchemaFieldTypes.NUMERIC
}),
client.hSet('1', 'field', '1'),
client.hSet('2', 'field', '2')
]);
assert.deepEqual(
await client.ft.search('index', '*', {
RETURN: []
}),
{
total: 2,
documents: [{
id: '1',
value: Object.create(null)
}, {
id: '2',
value: Object.create(null)
}]
}
);
}, GLOBAL.SERVERS.OPEN);
it('with VERBATIM', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
VERBATIM: true
}),
['FT.SEARCH', 'index', 'query', 'VERBATIM']
);
});
it('with NOSTOPWORDS', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
NOSTOPWORDS: true
}),
['FT.SEARCH', 'index', 'query', 'NOSTOPWORDS']
);
});
it('with INKEYS', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
INKEYS: 'key'
}),
['FT.SEARCH', 'index', 'query', 'INKEYS', '1', 'key']
);
});
it('with INFIELDS', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
INFIELDS: 'field'
}),
['FT.SEARCH', 'index', 'query', 'INFIELDS', '1', 'field']
);
});
it('with RETURN', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
RETURN: 'return'
}),
['FT.SEARCH', 'index', 'query', 'RETURN', '1', 'return']
);
});
describe('with SUMMARIZE', () => {
it('true', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
SUMMARIZE: true
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE']
);
});
describe('with FIELDS', () => {
it('string', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
SUMMARIZE: {
FIELDS: '@field'
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '1', '@field']
);
});
it('Array', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
SUMMARIZE: {
FIELDS: ['@1', '@2']
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FIELDS', '2', '@1', '@2']
);
});
});
it('with FRAGS', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
SUMMARIZE: {
FRAGS: 1
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'FRAGS', '1']
);
});
it('with LEN', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
SUMMARIZE: {
LEN: 1
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'LEN', '1']
);
});
it('with SEPARATOR', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
SUMMARIZE: {
SEPARATOR: 'separator'
}
}),
['FT.SEARCH', 'index', 'query', 'SUMMARIZE', 'SEPARATOR', 'separator']
);
});
});
describe('with HIGHLIGHT', () => {
it('true', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
HIGHLIGHT: true
}),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT']
);
});
describe('with FIELDS', () => {
it('string', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
HIGHLIGHT: {
FIELDS: ['@field']
}
}),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '1', '@field']
);
});
it('Array', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
HIGHLIGHT: {
FIELDS: ['@1', '@2']
}
}),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'FIELDS', '2', '@1', '@2']
);
});
});
it('with TAGS', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
HIGHLIGHT: {
TAGS: {
open: 'open',
close: 'close'
}
}
}),
['FT.SEARCH', 'index', 'query', 'HIGHLIGHT', 'TAGS', 'open', 'close']
);
});
});
it('with SLOP', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
SLOP: 1
}),
['FT.SEARCH', 'index', 'query', 'SLOP', '1']
);
});
it('with TIMEOUT', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
TIMEOUT: 1
}),
['FT.SEARCH', 'index', 'query', 'TIMEOUT', '1']
);
});
it('with INORDER', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
INORDER: true
}),
['FT.SEARCH', 'index', 'query', 'INORDER']
);
});
it('with LANGUAGE', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
LANGUAGE: 'Arabic'
}),
['FT.SEARCH', 'index', 'query', 'LANGUAGE', 'Arabic']
);
});
it('with EXPANDER', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
EXPANDER: 'expender'
}),
['FT.SEARCH', 'index', 'query', 'EXPANDER', 'expender']
);
});
it('with SCORER', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
SCORER: 'scorer'
}),
['FT.SEARCH', 'index', 'query', 'SCORER', 'scorer']
);
});
it('with SORTBY', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
SORTBY: '@by'
}),
['FT.SEARCH', 'index', 'query', 'SORTBY', '@by']
);
});
it('with LIMIT', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
LIMIT: {
from: 0,
size: 1
}
}),
['FT.SEARCH', 'index', 'query', 'LIMIT', '0', '1']
);
});
it('with PARAMS', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
PARAMS: {
string: 'string',
buffer: Buffer.from('buffer'),
number: 1
}
}),
['FT.SEARCH', 'index', 'query', 'PARAMS', '6', 'string', 'string', 'buffer', Buffer.from('buffer'), 'number', '1']
);
});
it('with DIALECT', () => {
assert.deepEqual(
SEARCH.transformArguments('index', 'query', {
DIALECT: 1
}),
['FT.SEARCH', 'index', 'query', 'DIALECT', '1']
);
});
});
describe('client.ft.search', () => {
testUtils.testWithClient('without optional options', async client => {
await Promise.all([
client.ft.create('index', {
field: 'TEXT'
}),
client.hSet('1', 'field', '1')
]);
assert.deepEqual(
await client.ft.search('index', '*'),
{
total: 1,
documents: [{
id: '1',
value: Object.create(null, {
field: {
value: '1',
configurable: true,
enumerable: true
}
})
}]
}
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('RETURN []', async client => {
await Promise.all([
client.ft.create('index', {
field: 'TEXT'
}),
client.hSet('1', 'field', '1'),
client.hSet('2', 'field', '2')
]);
assert.deepEqual(
await client.ft.search('index', '*', {
RETURN: []
}),
{
total: 2,
documents: [{
id: '1',
value: Object.create(null)
}, {
id: '2',
value: Object.create(null)
}]
}
);
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -1,109 +1,222 @@
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { pushSearchOptions, RedisSearchLanguages, Params, PropertyName, SortByProperty, SearchReply } from '.';
import { RedisArgument, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types';
import { RedisVariadicArgument, pushOptionalVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
import { RediSearchProperty, RediSearchLanguage } from './CREATE';
export const FIRST_KEY_INDEX = 1;
export type FtSearchParams = Record<string, RedisArgument | number>;
export const IS_READ_ONLY = true;
export function pushParamsArgument(args: Array<RedisArgument>, params?: FtSearchParams) {
if (params) {
const length = args.push('PARAMS', '');
for (const key in params) {
if (!Object.hasOwn(params, key)) continue;
export interface SearchOptions {
VERBATIM?: true;
NOSTOPWORDS?: true;
// WITHSCORES?: true;
// WITHPAYLOADS?: true;
WITHSORTKEYS?: true;
// FILTER?: {
// field: string;
// min: number | string;
// max: number | string;
// };
// GEOFILTER?: {
// field: string;
// lon: number;
// lat: number;
// radius: number;
// unit: 'm' | 'km' | 'mi' | 'ft';
// };
INKEYS?: string | Array<string>;
INFIELDS?: string | Array<string>;
RETURN?: string | Array<string>;
SUMMARIZE?: true | {
FIELDS?: PropertyName | Array<PropertyName>;
FRAGS?: number;
LEN?: number;
SEPARATOR?: string;
};
HIGHLIGHT?: true | {
FIELDS?: PropertyName | Array<PropertyName>;
TAGS?: {
open: string;
close: string;
};
};
SLOP?: number;
INORDER?: true;
LANGUAGE?: RedisSearchLanguages;
EXPANDER?: string;
SCORER?: string;
// EXPLAINSCORE?: true; // TODO: WITHSCORES
// PAYLOAD?: ;
SORTBY?: SortByProperty;
// MSORTBY?: SortByProperty | Array<SortByProperty>;
LIMIT?: {
from: number | string;
size: number | string;
};
PARAMS?: Params;
DIALECT?: number;
TIMEOUT?: number;
const value = params[key];
args.push(
key,
typeof value === 'number' ? value.toString() : value
);
}
args[length - 1] = (args.length - length).toString();
}
}
export function transformArguments(
index: string,
query: string,
options?: SearchOptions
): RedisCommandArguments {
return pushSearchOptions(
['FT.SEARCH', index, query],
options
);
export interface FtSearchOptions {
VERBATIM?: boolean;
NOSTOPWORDS?: boolean;
INKEYS?: RedisVariadicArgument;
INFIELDS?: RedisVariadicArgument;
RETURN?: RedisVariadicArgument;
SUMMARIZE?: boolean | {
FIELDS?: RediSearchProperty | Array<RediSearchProperty>;
FRAGS?: number;
LEN?: number;
SEPARATOR?: RedisArgument;
};
HIGHLIGHT?: boolean | {
FIELDS?: RediSearchProperty | Array<RediSearchProperty>;
TAGS?: {
open: RedisArgument;
close: RedisArgument;
};
};
SLOP?: number;
TIMEOUT?: number;
INORDER?: boolean;
LANGUAGE?: RediSearchLanguage;
EXPANDER?: RedisArgument;
SCORER?: RedisArgument;
SORTBY?: RedisArgument | {
BY: RediSearchProperty;
DIRECTION?: 'ASC' | 'DESC';
};
LIMIT?: {
from: number | RedisArgument;
size: number | RedisArgument;
};
PARAMS?: FtSearchParams;
DIALECT?: number;
}
export function pushSearchOptions(args: Array<RedisArgument>, options?: FtSearchOptions) {
if (options?.VERBATIM) {
args.push('VERBATIM');
}
if (options?.NOSTOPWORDS) {
args.push('NOSTOPWORDS');
}
pushOptionalVariadicArgument(args, 'INKEYS', options?.INKEYS);
pushOptionalVariadicArgument(args, 'INFIELDS', options?.INFIELDS);
pushOptionalVariadicArgument(args, 'RETURN', options?.RETURN);
if (options?.SUMMARIZE) {
args.push('SUMMARIZE');
if (typeof options.SUMMARIZE === 'object') {
pushOptionalVariadicArgument(args, 'FIELDS', options.SUMMARIZE.FIELDS);
if (options.SUMMARIZE.FRAGS !== undefined) {
args.push('FRAGS', options.SUMMARIZE.FRAGS.toString());
}
if (options.SUMMARIZE.LEN !== undefined) {
args.push('LEN', options.SUMMARIZE.LEN.toString());
}
if (options.SUMMARIZE.SEPARATOR !== undefined) {
args.push('SEPARATOR', options.SUMMARIZE.SEPARATOR);
}
}
}
if (options?.HIGHLIGHT) {
args.push('HIGHLIGHT');
if (typeof options.HIGHLIGHT === 'object') {
pushOptionalVariadicArgument(args, 'FIELDS', options.HIGHLIGHT.FIELDS);
if (options.HIGHLIGHT.TAGS) {
args.push('TAGS', options.HIGHLIGHT.TAGS.open, options.HIGHLIGHT.TAGS.close);
}
}
}
if (options?.SLOP !== undefined) {
args.push('SLOP', options.SLOP.toString());
}
if (options?.TIMEOUT !== undefined) {
args.push('TIMEOUT', options.TIMEOUT.toString());
}
if (options?.INORDER) {
args.push('INORDER');
}
if (options?.LANGUAGE) {
args.push('LANGUAGE', options.LANGUAGE);
}
if (options?.EXPANDER) {
args.push('EXPANDER', options.EXPANDER);
}
if (options?.SCORER) {
args.push('SCORER', options.SCORER);
}
if (options?.SORTBY) {
args.push('SORTBY');
if (typeof options.SORTBY === 'string' || options.SORTBY instanceof Buffer) {
args.push(options.SORTBY);
} else {
args.push(options.SORTBY.BY);
if (options.SORTBY.DIRECTION) {
args.push(options.SORTBY.DIRECTION);
}
}
}
if (options?.LIMIT) {
args.push('LIMIT', options.LIMIT.from.toString(), options.LIMIT.size.toString());
}
pushParamsArgument(args, options?.PARAMS);
if (options?.DIALECT !== undefined) {
args.push('DIALECT', options.DIALECT.toString());
}
return args;
}
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(index: RedisArgument, query: RedisArgument, options?: FtSearchOptions) {
const args = ['FT.SEARCH', index, query];
return pushSearchOptions(args, options);
},
transformReply: {
2: (reply: SearchRawReply): SearchReply => {
const withoutDocuments = (reply[0] + 1 == reply.length)
const documents = [];
let i = 1;
while (i < reply.length) {
documents.push({
id: reply[i++],
value: withoutDocuments ? Object.create(null) : documentValue(reply[i++])
});
}
return {
total: reply[0],
documents
};
},
3: undefined as unknown as () => ReplyUnion
},
unstableResp3: true
} as const satisfies Command;
export type SearchRawReply = Array<any>;
export function transformReply(reply: SearchRawReply, withoutDocuments: boolean): SearchReply {
const documents = [];
let i = 1;
while (i < reply.length) {
documents.push({
id: reply[i++],
value: withoutDocuments ? Object.create(null) : documentValue(reply[i++])
});
}
interface SearchDocumentValue {
[key: string]: string | number | null | Array<SearchDocumentValue> | SearchDocumentValue;
}
return {
total: reply[0],
documents
};
export interface SearchReply {
total: number;
documents: Array<{
id: string;
value: SearchDocumentValue;
}>;
}
function documentValue(tuples: any) {
const message = Object.create(null);
const message = Object.create(null);
let i = 0;
while (i < tuples.length) {
const key = tuples[i++],
value = tuples[i++];
if (key === '$') { // might be a JSON reply
try {
Object.assign(message, JSON.parse(value));
continue;
} catch {
// set as a regular property if not a valid JSON
}
}
let i = 0;
while (i < tuples.length) {
const key = tuples[i++],
value = tuples[i++];
if (key === '$') { // might be a JSON reply
try {
Object.assign(message, JSON.parse(value));
continue;
} catch {
// set as a regular property if not a valid JSON
}
}
message[key] = value;
}
message[key] = value;
}
return message;
return message;
}

View File

@@ -1,45 +1,34 @@
import { strict as assert } from 'assert';
import { SchemaFieldTypes } from '.';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments, transformReply } from './SEARCH_NOCONTENT';
import SEARCH_NOCONTENT from './SEARCH_NOCONTENT';
describe('SEARCH_NOCONTENT', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', 'query'),
['FT.SEARCH', 'index', 'query', 'NOCONTENT']
);
});
describe('FT.SEARCH NOCONTENT', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
SEARCH_NOCONTENT.transformArguments('index', 'query'),
['FT.SEARCH', 'index', 'query', 'NOCONTENT']
);
});
});
describe('transformReply', () => {
it('returns total and keys', () => {
assert.deepEqual(transformReply([3, '1', '2', '3']), {
total: 3,
documents: ['1', '2', '3']
})
});
});
describe('client.ft.searchNoContent', () => {
testUtils.testWithClient('returns total and keys', async client => {
await Promise.all([
client.ft.create('index', {
field: 'TEXT'
}),
client.hSet('1', 'field', 'field1'),
client.hSet('2', 'field', 'field2')
]);
describe('client.ft.searchNoContent', () => {
testUtils.testWithClient('returns total and keys', async client => {
await Promise.all([
client.ft.create('index', {
field: SchemaFieldTypes.TEXT
}),
client.hSet('1', 'field', 'field1'),
client.hSet('2', 'field', 'field2'),
client.hSet('3', 'field', 'field3')
]);
assert.deepEqual(
await client.ft.searchNoContent('index', '*'),
{
total: 3,
documents: ['1','2','3']
}
);
}, GLOBAL.SERVERS.OPEN);
});
assert.deepEqual(
await client.ft.searchNoContent('index', '*'),
{
total: 2,
documents: ['1', '2']
}
);
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -1,30 +1,27 @@
import { RedisCommandArguments } from "@redis/client/dist/lib/commands";
import { pushSearchOptions } from ".";
import { SearchOptions, SearchRawReply } from "./SEARCH";
import { Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types';
import SEARCH, { SearchRawReply } from './SEARCH';
export const FIRST_KEY_INDEX = 1;
export const IS_READ_ONLY = true;
export function transformArguments(
index: string,
query: string,
options?: SearchOptions
): RedisCommandArguments {
return pushSearchOptions(
['FT.SEARCH', index, query, 'NOCONTENT'],
options
);
}
export interface SearchNoContentReply {
total: number;
documents: Array<string>;
};
export function transformReply(reply: SearchRawReply): SearchNoContentReply {
return {
export default {
FIRST_KEY_INDEX: SEARCH.FIRST_KEY_INDEX,
IS_READ_ONLY: SEARCH.IS_READ_ONLY,
transformArguments(...args: Parameters<typeof SEARCH.transformArguments>) {
const redisArgs = SEARCH.transformArguments(...args);
redisArgs.push('NOCONTENT');
return redisArgs;
},
transformReply: {
2: (reply: SearchRawReply): SearchNoContentReply => {
return {
total: reply[0],
documents: reply.slice(1)
};
}
}
},
3: undefined as unknown as () => ReplyUnion
},
unstableResp3: true
} as const satisfies Command;
export interface SearchNoContentReply {
total: number;
documents: Array<string>;
};

View File

@@ -1,80 +1,79 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { SchemaFieldTypes } from '.';
import { transformArguments } from './SPELLCHECK';
import SPELLCHECK from './SPELLCHECK';
describe('SPELLCHECK', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('index', 'query'),
['FT.SPELLCHECK', 'index', 'query']
);
});
it('with DISTANCE', () => {
assert.deepEqual(
transformArguments('index', 'query', { DISTANCE: 2 }),
['FT.SPELLCHECK', 'index', 'query', 'DISTANCE', '2']
);
});
describe('with TERMS', () => {
it('single', () => {
assert.deepEqual(
transformArguments('index', 'query', {
TERMS: {
mode: 'INCLUDE',
dictionary: 'dictionary'
}
}),
['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'dictionary']
);
});
it('multiple', () => {
assert.deepEqual(
transformArguments('index', 'query', {
TERMS: [{
mode: 'INCLUDE',
dictionary: 'include'
}, {
mode: 'EXCLUDE',
dictionary: 'exclude'
}]
}),
['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'include', 'TERMS', 'EXCLUDE', 'exclude']
);
});
});
it('with DIALECT', () => {
assert.deepEqual(
transformArguments('index', 'query', {
DIALECT: 1
}),
['FT.SPELLCHECK', 'index', 'query', 'DIALECT', '1']
);
});
describe('FT.SPELLCHECK', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
SPELLCHECK.transformArguments('index', 'query'),
['FT.SPELLCHECK', 'index', 'query']
);
});
testUtils.testWithClient('client.ft.spellCheck', async client => {
await Promise.all([
client.ft.create('index', {
field: SchemaFieldTypes.TEXT
}),
client.hSet('key', 'field', 'query')
]);
it('with DISTANCE', () => {
assert.deepEqual(
SPELLCHECK.transformArguments('index', 'query', {
DISTANCE: 2
}),
['FT.SPELLCHECK', 'index', 'query', 'DISTANCE', '2']
);
});
describe('with TERMS', () => {
it('single', () => {
assert.deepEqual(
await client.ft.spellCheck('index', 'quer'),
[{
term: 'quer',
suggestions: [{
score: 1,
suggestion: 'query'
}]
}]
SPELLCHECK.transformArguments('index', 'query', {
TERMS: {
mode: 'INCLUDE',
dictionary: 'dictionary'
}
}),
['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'dictionary']
);
}, GLOBAL.SERVERS.OPEN);
});
it('multiple', () => {
assert.deepEqual(
SPELLCHECK.transformArguments('index', 'query', {
TERMS: [{
mode: 'INCLUDE',
dictionary: 'include'
}, {
mode: 'EXCLUDE',
dictionary: 'exclude'
}]
}),
['FT.SPELLCHECK', 'index', 'query', 'TERMS', 'INCLUDE', 'include', 'TERMS', 'EXCLUDE', 'exclude']
);
});
});
it('with DIALECT', () => {
assert.deepEqual(
SPELLCHECK.transformArguments('index', 'query', {
DIALECT: 1
}),
['FT.SPELLCHECK', 'index', 'query', 'DIALECT', '1']
);
});
});
testUtils.testWithClient('client.ft.spellCheck', async client => {
const [,, reply] = await Promise.all([
client.ft.create('index', {
field: 'TEXT'
}),
client.hSet('key', 'field', 'query'),
client.ft.spellCheck('index', 'quer')
]);
assert.deepEqual(reply, [{
term: 'quer',
suggestions: [{
score: 1,
suggestion: 'query'
}]
}]);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,62 +1,71 @@
interface SpellCheckTerms {
mode: 'INCLUDE' | 'EXCLUDE';
dictionary: string;
import { RedisArgument, CommandArguments, Command, ReplyUnion } from '@redis/client/dist/lib/RESP/types';
export interface Terms {
mode: 'INCLUDE' | 'EXCLUDE';
dictionary: RedisArgument;
}
interface SpellCheckOptions {
DISTANCE?: number;
TERMS?: SpellCheckTerms | Array<SpellCheckTerms>;
DIALECT?: number;
export interface FtSpellCheckOptions {
DISTANCE?: number;
TERMS?: Terms | Array<Terms>;
DIALECT?: number;
}
export function transformArguments(index: string, query: string, options?: SpellCheckOptions): Array<string> {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(index: RedisArgument, query: RedisArgument, options?: FtSpellCheckOptions) {
const args = ['FT.SPELLCHECK', index, query];
if (options?.DISTANCE) {
args.push('DISTANCE', options.DISTANCE.toString());
args.push('DISTANCE', options.DISTANCE.toString());
}
if (options?.TERMS) {
if (Array.isArray(options.TERMS)) {
for (const term of options.TERMS) {
pushTerms(args, term);
}
} else {
pushTerms(args, options.TERMS);
if (Array.isArray(options.TERMS)) {
for (const term of options.TERMS) {
pushTerms(args, term);
}
} else {
pushTerms(args, options.TERMS);
}
}
if (options?.DIALECT) {
args.push('DIALECT', options.DIALECT.toString());
args.push('DIALECT', options.DIALECT.toString());
}
return args;
}
function pushTerms(args: Array<string>, { mode, dictionary }: SpellCheckTerms): void {
args.push('TERMS', mode, dictionary);
}
},
transformReply: {
2: (rawReply: SpellCheckRawReply): SpellCheckReply => {
return rawReply.map(([, term, suggestions]) => ({
term,
suggestions: suggestions.map(([score, suggestion]) => ({
score: Number(score),
suggestion
}))
}));
},
3: undefined as unknown as () => ReplyUnion,
},
unstableResp3: true
} as const satisfies Command;
type SpellCheckRawReply = Array<[
_: string,
term: string,
suggestions: Array<[score: string, suggestion: string]>
_: string,
term: string,
suggestions: Array<[score: string, suggestion: string]>
]>;
type SpellCheckReply = Array<{
term: string,
suggestions: Array<{
score: number,
suggestion: string
}>
term: string,
suggestions: Array<{
score: number,
suggestion: string
}>
}>;
export function transformReply(rawReply: SpellCheckRawReply): SpellCheckReply {
return rawReply.map(([, term, suggestions]) => ({
term,
suggestions: suggestions.map(([score, suggestion]) => ({
score: Number(score),
suggestion
}))
}));
function pushTerms(args: CommandArguments, { mode, dictionary }: Terms) {
args.push('TERMS', mode, dictionary);
}

View File

@@ -1,35 +1,35 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGADD';
import SUGADD from './SUGADD';
describe('SUGADD', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('key', 'string', 1),
['FT.SUGADD', 'key', 'string', '1']
);
});
it('with INCR', () => {
assert.deepEqual(
transformArguments('key', 'string', 1, { INCR: true }),
['FT.SUGADD', 'key', 'string', '1', 'INCR']
);
});
it('with PAYLOAD', () => {
assert.deepEqual(
transformArguments('key', 'string', 1, { PAYLOAD: 'payload' }),
['FT.SUGADD', 'key', 'string', '1', 'PAYLOAD', 'payload']
);
});
describe('FT.SUGADD', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
SUGADD.transformArguments('key', 'string', 1),
['FT.SUGADD', 'key', 'string', '1']
);
});
testUtils.testWithClient('client.ft.sugAdd', async client => {
assert.equal(
await client.ft.sugAdd('key', 'string', 1),
1
);
}, GLOBAL.SERVERS.OPEN);
it('with INCR', () => {
assert.deepEqual(
SUGADD.transformArguments('key', 'string', 1, { INCR: true }),
['FT.SUGADD', 'key', 'string', '1', 'INCR']
);
});
it('with PAYLOAD', () => {
assert.deepEqual(
SUGADD.transformArguments('key', 'string', 1, { PAYLOAD: 'payload' }),
['FT.SUGADD', 'key', 'string', '1', 'PAYLOAD', 'payload']
);
});
});
testUtils.testWithClient('client.ft.sugAdd', async client => {
assert.equal(
await client.ft.sugAdd('key', 'string', 1),
1
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,20 +1,25 @@
interface SugAddOptions {
INCR?: true;
PAYLOAD?: string;
import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
export interface FtSugAddOptions {
INCR?: boolean;
PAYLOAD?: RedisArgument;
}
export function transformArguments(key: string, string: string, score: number, options?: SugAddOptions): Array<string> {
export default {
FIRST_KEY_INDEX: 1,
IS_READ_ONLY: true,
transformArguments(key: RedisArgument, string: RedisArgument, score: number, options?: FtSugAddOptions) {
const args = ['FT.SUGADD', key, string, score.toString()];
if (options?.INCR) {
args.push('INCR');
args.push('INCR');
}
if (options?.PAYLOAD) {
args.push('PAYLOAD', options.PAYLOAD);
args.push('PAYLOAD', options.PAYLOAD);
}
return args;
}
export declare function transformReply(): number;
},
transformReply: undefined as unknown as () => NumberReply
} as const satisfies Command;

View File

@@ -1,19 +1,19 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGDEL';
import SUGDEL from './SUGDEL';
describe('SUGDEL', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'string'),
['FT.SUGDEL', 'key', 'string']
);
});
describe('FT.SUGDEL', () => {
it('transformArguments', () => {
assert.deepEqual(
SUGDEL.transformArguments('key', 'string'),
['FT.SUGDEL', 'key', 'string']
);
});
testUtils.testWithClient('client.ft.sugDel', async client => {
assert.equal(
await client.ft.sugDel('key', 'string'),
false
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('client.ft.sugDel', async client => {
assert.equal(
await client.ft.sugDel('key', 'string'),
0
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,5 +1,10 @@
export function transformArguments(key: string, string: string): Array<string> {
return ['FT.SUGDEL', key, string];
}
import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
export { transformBooleanReply as transformReply } from '@redis/client/dist/lib/commands/generic-transformers';
export default {
FIRST_KEY_INDEX: 1,
IS_READ_ONLY: true,
transformArguments(key: RedisArgument, string: RedisArgument) {
return ['FT.SUGDEL', key, string];
},
transformReply: undefined as unknown as () => NumberReply<0 | 1>
} as const satisfies Command;

View File

@@ -1,46 +1,46 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGGET';
import SUGGET from './SUGGET';
describe('SUGGET', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix']
);
});
it('with FUZZY', () => {
assert.deepEqual(
transformArguments('key', 'prefix', { FUZZY: true }),
['FT.SUGGET', 'key', 'prefix', 'FUZZY']
);
});
it('with MAX', () => {
assert.deepEqual(
transformArguments('key', 'prefix', { MAX: 10 }),
['FT.SUGGET', 'key', 'prefix', 'MAX', '10']
);
});
describe('FT.SUGGET', () => {
describe('transformArguments', () => {
it('without options', () => {
assert.deepEqual(
SUGGET.transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix']
);
});
describe('client.ft.sugGet', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGet('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('with suggestions', async client => {
await client.ft.sugAdd('key', 'string', 1);
assert.deepEqual(
await client.ft.sugGet('key', 'string'),
['string']
);
}, GLOBAL.SERVERS.OPEN);
it('with FUZZY', () => {
assert.deepEqual(
SUGGET.transformArguments('key', 'prefix', { FUZZY: true }),
['FT.SUGGET', 'key', 'prefix', 'FUZZY']
);
});
it('with MAX', () => {
assert.deepEqual(
SUGGET.transformArguments('key', 'prefix', { MAX: 10 }),
['FT.SUGGET', 'key', 'prefix', 'MAX', '10']
);
});
});
describe('client.ft.sugGet', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGet('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('with suggestions', async client => {
const [, reply] = await Promise.all([
client.ft.sugAdd('key', 'string', 1),
client.ft.sugGet('key', 's')
]);
assert.deepEqual(reply, ['string']);
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -1,22 +1,25 @@
export const IS_READ_ONLY = true;
import { NullReply, ArrayReply, BlobStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
export interface SugGetOptions {
FUZZY?: true;
MAX?: number;
export interface FtSugGetOptions {
FUZZY?: boolean;
MAX?: number;
}
export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
export default {
FIRST_KEY_INDEX: 1,
IS_READ_ONLY: true,
transformArguments(key: RedisArgument, prefix: RedisArgument, options?: FtSugGetOptions) {
const args = ['FT.SUGGET', key, prefix];
if (options?.FUZZY) {
args.push('FUZZY');
args.push('FUZZY');
}
if (options?.MAX) {
args.push('MAX', options.MAX.toString());
if (options?.MAX !== undefined) {
args.push('MAX', options.MAX.toString());
}
return args;
}
export declare function transformReply(): null | Array<string>;
},
transformReply: undefined as unknown as () => NullReply | ArrayReply<BlobStringReply>
} as const satisfies Command;

View File

@@ -1,33 +1,35 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGGET_WITHPAYLOADS';
import SUGGET_WITHPAYLOADS from './SUGGET_WITHPAYLOADS';
describe('SUGGET WITHPAYLOADS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix', 'WITHPAYLOADS']
);
});
describe('FT.SUGGET WITHPAYLOADS', () => {
it('transformArguments', () => {
assert.deepEqual(
SUGGET_WITHPAYLOADS.transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix', 'WITHPAYLOADS']
);
});
describe('client.ft.sugGetWithPayloads', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGetWithPayloads('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
describe('client.ft.sugGetWithPayloads', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGetWithPayloads('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('with suggestions', async client => {
await client.ft.sugAdd('key', 'string', 1, { PAYLOAD: 'payload' });
testUtils.testWithClient('with suggestions', async client => {
const [, reply] = await Promise.all([
client.ft.sugAdd('key', 'string', 1, {
PAYLOAD: 'payload'
}),
client.ft.sugGetWithPayloads('key', 'string')
]);
assert.deepEqual(
await client.ft.sugGetWithPayloads('key', 'string'),
[{
suggestion: 'string',
payload: 'payload'
}]
);
}, GLOBAL.SERVERS.OPEN);
});
assert.deepEqual(reply, [{
suggestion: 'string',
payload: 'payload'
}]);
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -1,29 +1,31 @@
import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET';
import { NullReply, ArrayReply, BlobStringReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types';
import { isNullReply } from '@redis/client/dist/lib/commands/generic-transformers';
import SUGGET from './SUGGET';
export { IS_READ_ONLY } from './SUGGET';
export default {
FIRST_KEY_INDEX: SUGGET.FIRST_KEY_INDEX,
IS_READ_ONLY: SUGGET.IS_READ_ONLY,
transformArguments(...args: Parameters<typeof SUGGET.transformArguments>) {
const transformedArguments = SUGGET.transformArguments(...args);
transformedArguments.push('WITHPAYLOADS');
return transformedArguments;
},
transformReply(reply: NullReply | UnwrapReply<ArrayReply<BlobStringReply>>) {
if (isNullReply(reply)) return null;
export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
return [
...transformSugGetArguments(key, prefix, options),
'WITHPAYLOADS'
];
}
export interface SuggestionWithPayload {
suggestion: string;
payload: string | null;
}
export function transformReply(rawReply: Array<string | null> | null): Array<SuggestionWithPayload> | null {
if (rawReply === null) return null;
const transformedReply = [];
for (let i = 0; i < rawReply.length; i += 2) {
transformedReply.push({
suggestion: rawReply[i]!,
payload: rawReply[i + 1]
});
const transformedReply: Array<{
suggestion: BlobStringReply;
payload: BlobStringReply;
}> = new Array(reply.length / 2);
let replyIndex = 0,
arrIndex = 0;
while (replyIndex < reply.length) {
transformedReply[arrIndex++] = {
suggestion: reply[replyIndex++],
payload: reply[replyIndex++]
};
}
return transformedReply;
}
}
} as const satisfies Command;

View File

@@ -1,33 +1,33 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGGET_WITHSCORES';
import SUGGET_WITHSCORES from './SUGGET_WITHSCORES';
describe('SUGGET WITHSCORES', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix', 'WITHSCORES']
);
});
describe('FT.SUGGET WITHSCORES', () => {
it('transformArguments', () => {
assert.deepEqual(
SUGGET_WITHSCORES.transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix', 'WITHSCORES']
);
});
describe('client.ft.sugGetWithScores', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGetWithScores('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
describe('client.ft.sugGetWithScores', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGetWithScores('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('with suggestions', async client => {
await client.ft.sugAdd('key', 'string', 1);
testUtils.testWithClient('with suggestions', async client => {
const [, reply] = await Promise.all([
client.ft.sugAdd('key', 'string', 1),
client.ft.sugGetWithScores('key', 's')
]);
assert.deepEqual(
await client.ft.sugGetWithScores('key', 'string'),
[{
suggestion: 'string',
score: 2147483648
}]
);
}, GLOBAL.SERVERS.OPEN);
});
assert.ok(Array.isArray(reply));
assert.equal(reply.length, 1);
assert.equal(reply[0].suggestion, 'string');
assert.equal(typeof reply[0].score, 'number');
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -1,29 +1,50 @@
import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET';
import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/dist/lib/RESP/types';
import { isNullReply, transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers';
import SUGGET from './SUGGET';
export { IS_READ_ONLY } from './SUGGET';
export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
return [
...transformSugGetArguments(key, prefix, options),
'WITHSCORES'
];
type SuggestScore = {
suggestion: BlobStringReply;
score: DoubleReply;
}
export interface SuggestionWithScores {
suggestion: string;
score: number;
}
export default {
FIRST_KEY_INDEX: SUGGET.FIRST_KEY_INDEX,
IS_READ_ONLY: SUGGET.IS_READ_ONLY,
transformArguments(...args: Parameters<typeof SUGGET.transformArguments>) {
const transformedArguments = SUGGET.transformArguments(...args);
transformedArguments.push('WITHSCORES');
return transformedArguments;
},
transformReply: {
2: (reply: NullReply | UnwrapReply<ArrayReply<BlobStringReply>>, preserve?: any, typeMapping?: TypeMapping) => {
if (isNullReply(reply)) return null;
export function transformReply(rawReply: Array<string> | null): Array<SuggestionWithScores> | null {
if (rawReply === null) return null;
const transformedReply: Array<SuggestScore> = new Array(reply.length / 2);
let replyIndex = 0,
arrIndex = 0;
while (replyIndex < reply.length) {
transformedReply[arrIndex++] = {
suggestion: reply[replyIndex++],
score: transformDoubleReply[2](reply[replyIndex++], preserve, typeMapping)
};
}
const transformedReply = [];
for (let i = 0; i < rawReply.length; i += 2) {
transformedReply.push({
suggestion: rawReply[i],
score: Number(rawReply[i + 1])
});
return transformedReply;
},
3: (reply: UnwrapReply<ArrayReply<BlobStringReply | DoubleReply>>) => {
if (isNullReply(reply)) return null;
const transformedReply: Array<SuggestScore> = new Array(reply.length / 2);
let replyIndex = 0,
arrIndex = 0;
while (replyIndex < reply.length) {
transformedReply[arrIndex++] = {
suggestion: reply[replyIndex++] as BlobStringReply,
score: reply[replyIndex++] as DoubleReply
};
}
return transformedReply;
}
return transformedReply;
}
}
} as const satisfies Command;

View File

@@ -1,34 +1,36 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGGET_WITHSCORES_WITHPAYLOADS';
import SUGGET_WITHSCORES_WITHPAYLOADS from './SUGGET_WITHSCORES_WITHPAYLOADS';
describe('SUGGET WITHSCORES WITHPAYLOADS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix', 'WITHSCORES', 'WITHPAYLOADS']
);
});
describe('FT.SUGGET WITHSCORES WITHPAYLOADS', () => {
it('transformArguments', () => {
assert.deepEqual(
SUGGET_WITHSCORES_WITHPAYLOADS.transformArguments('key', 'prefix'),
['FT.SUGGET', 'key', 'prefix', 'WITHSCORES', 'WITHPAYLOADS']
);
});
describe('client.ft.sugGetWithScoresWithPayloads', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGetWithScoresWithPayloads('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
describe('client.ft.sugGetWithScoresWithPayloads', () => {
testUtils.testWithClient('null', async client => {
assert.equal(
await client.ft.sugGetWithScoresWithPayloads('key', 'prefix'),
null
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('with suggestions', async client => {
await client.ft.sugAdd('key', 'string', 1, { PAYLOAD: 'payload' });
testUtils.testWithClient('with suggestions', async client => {
const [, reply] = await Promise.all([
client.ft.sugAdd('key', 'string', 1, {
PAYLOAD: 'payload'
}),
client.ft.sugGetWithScoresWithPayloads('key', 'string')
]);
assert.deepEqual(
await client.ft.sugGetWithScoresWithPayloads('key', 'string'),
[{
suggestion: 'string',
score: 2147483648,
payload: 'payload'
}]
);
}, GLOBAL.SERVERS.OPEN);
});
assert.ok(Array.isArray(reply));
assert.equal(reply.length, 1);
assert.equal(reply[0].suggestion, 'string');
assert.equal(typeof reply[0].score, 'number');
assert.equal(reply[0].payload, 'payload');
}, GLOBAL.SERVERS.OPEN);
});
});

View File

@@ -1,30 +1,56 @@
import { SugGetOptions, transformArguments as transformSugGetArguments } from './SUGGET';
import { SuggestionWithPayload } from './SUGGET_WITHPAYLOADS';
import { SuggestionWithScores } from './SUGGET_WITHSCORES';
import { NullReply, ArrayReply, BlobStringReply, DoubleReply, UnwrapReply, Command, TypeMapping } from '@redis/client/dist/lib/RESP/types';
import { isNullReply, transformDoubleReply } from '@redis/client/dist/lib/commands/generic-transformers';
import SUGGET from './SUGGET';
export { IS_READ_ONLY } from './SUGGET';
export function transformArguments(key: string, prefix: string, options?: SugGetOptions): Array<string> {
return [
...transformSugGetArguments(key, prefix, options),
'WITHSCORES',
'WITHPAYLOADS'
];
type SuggestScoreWithPayload = {
suggestion: BlobStringReply;
score: DoubleReply;
payload: BlobStringReply;
}
type SuggestionWithScoresAndPayloads = SuggestionWithScores & SuggestionWithPayload;
export default {
FIRST_KEY_INDEX: SUGGET.FIRST_KEY_INDEX,
IS_READ_ONLY: SUGGET.IS_READ_ONLY,
transformArguments(...args: Parameters<typeof SUGGET.transformArguments>) {
const transformedArguments = SUGGET.transformArguments(...args);
transformedArguments.push(
'WITHSCORES',
'WITHPAYLOADS'
);
return transformedArguments;
},
transformReply: {
2: (reply: NullReply | UnwrapReply<ArrayReply<BlobStringReply>>, preserve?: any, typeMapping?: TypeMapping) => {
if (isNullReply(reply)) return null;
export function transformReply(rawReply: Array<string | null> | null): Array<SuggestionWithScoresAndPayloads> | null {
if (rawReply === null) return null;
const transformedReply: Array<SuggestScoreWithPayload> = new Array(reply.length / 3);
let replyIndex = 0,
arrIndex = 0;
while (replyIndex < reply.length) {
transformedReply[arrIndex++] = {
suggestion: reply[replyIndex++],
score: transformDoubleReply[2](reply[replyIndex++], preserve, typeMapping),
payload: reply[replyIndex++]
};
}
const transformedReply = [];
for (let i = 0; i < rawReply.length; i += 3) {
transformedReply.push({
suggestion: rawReply[i]!,
score: Number(rawReply[i + 1]!),
payload: rawReply[i + 2]
});
return transformedReply;
},
3: (reply: NullReply | UnwrapReply<ArrayReply<BlobStringReply | DoubleReply>>) => {
if (isNullReply(reply)) return null;
const transformedReply: Array<SuggestScoreWithPayload> = new Array(reply.length / 3);
let replyIndex = 0,
arrIndex = 0;
while (replyIndex < reply.length) {
transformedReply[arrIndex++] = {
suggestion: reply[replyIndex++] as BlobStringReply,
score: reply[replyIndex++] as DoubleReply,
payload: reply[replyIndex++] as BlobStringReply
};
}
return transformedReply;
}
return transformedReply;
}
}
} as const satisfies Command;

View File

@@ -1,19 +1,19 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SUGLEN';
import SUGLEN from './SUGLEN';
describe('SUGLEN', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('key'),
['FT.SUGLEN', 'key']
);
});
describe('FT.SUGLEN', () => {
it('transformArguments', () => {
assert.deepEqual(
SUGLEN.transformArguments('key'),
['FT.SUGLEN', 'key']
);
});
testUtils.testWithClient('client.ft.sugLen', async client => {
assert.equal(
await client.ft.sugLen('key'),
0
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('client.ft.sugLen', async client => {
assert.equal(
await client.ft.sugLen('key'),
0
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,7 +1,10 @@
export const IS_READ_ONLY = true;
import { RedisArgument, NumberReply, Command } from '@redis/client/dist/lib/RESP/types';
export function transformArguments(key: string): Array<string> {
export default {
FIRST_KEY_INDEX: 1,
IS_READ_ONLY: true,
transformArguments(key: RedisArgument) {
return ['FT.SUGLEN', key];
}
export declare function transformReply(): number;
},
transformReply: undefined as unknown as () => NumberReply
} as const satisfies Command;

View File

@@ -1,24 +1,24 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SYNDUMP';
import { SchemaFieldTypes } from '.';
import SYNDUMP from './SYNDUMP';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('SYNDUMP', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('index'),
['FT.SYNDUMP', 'index']
);
});
describe('FT.SYNDUMP', () => {
it('transformArguments', () => {
assert.deepEqual(
SYNDUMP.transformArguments('index'),
['FT.SYNDUMP', 'index']
);
});
testUtils.testWithClient('client.ft.synDump', async client => {
await client.ft.create('index', {
field: SchemaFieldTypes.TEXT
});
testUtils.testWithClient('client.ft.synDump', async client => {
const [, reply] = await Promise.all([
client.ft.create('index', {
field: SCHEMA_FIELD_TYPE.TEXT
}),
client.ft.synDump('index')
]);
assert.deepEqual(
await client.ft.synDump('index'),
[]
);
}, GLOBAL.SERVERS.OPEN);
assert.deepEqual(reply, {});
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,5 +1,22 @@
export function transformArguments(index: string): Array<string> {
return ['FT.SYNDUMP', index];
}
import { RedisArgument, MapReply, BlobStringReply, ArrayReply, UnwrapReply, Command } from '@redis/client/dist/lib/RESP/types';
export declare function transformReply(): Array<string>;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(index: RedisArgument) {
return ['FT.SYNDUMP', index];
},
transformReply: {
2: (reply: UnwrapReply<ArrayReply<BlobStringReply | ArrayReply<BlobStringReply>>>) => {
const result: Record<string, ArrayReply<BlobStringReply>> = {};
let i = 0;
while (i < reply.length) {
const key = (reply[i++] as unknown as UnwrapReply<BlobStringReply>).toString(),
value = reply[i++] as unknown as ArrayReply<BlobStringReply>;
result[key] = value;
}
return result;
},
3: undefined as unknown as () => MapReply<BlobStringReply, ArrayReply<BlobStringReply>>
}
} as const satisfies Command;

View File

@@ -1,40 +1,42 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './SYNUPDATE';
import { SchemaFieldTypes } from '.';
import SYNUPDATE from './SYNUPDATE';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('SYNUPDATE', () => {
describe('transformArguments', () => {
it('single term', () => {
assert.deepEqual(
transformArguments('index', 'groupId', 'term'),
['FT.SYNUPDATE', 'index', 'groupId', 'term']
);
});
it('multiple terms', () => {
assert.deepEqual(
transformArguments('index', 'groupId', ['1', '2']),
['FT.SYNUPDATE', 'index', 'groupId', '1', '2']
);
});
it('with SKIPINITIALSCAN', () => {
assert.deepEqual(
transformArguments('index', 'groupId', 'term', { SKIPINITIALSCAN: true }),
['FT.SYNUPDATE', 'index', 'groupId', 'SKIPINITIALSCAN', 'term']
);
});
describe('FT.SYNUPDATE', () => {
describe('transformArguments', () => {
it('single term', () => {
assert.deepEqual(
SYNUPDATE.transformArguments('index', 'groupId', 'term'),
['FT.SYNUPDATE', 'index', 'groupId', 'term']
);
});
testUtils.testWithClient('client.ft.synUpdate', async client => {
await client.ft.create('index', {
field: SchemaFieldTypes.TEXT
});
it('multiple terms', () => {
assert.deepEqual(
SYNUPDATE.transformArguments('index', 'groupId', ['1', '2']),
['FT.SYNUPDATE', 'index', 'groupId', '1', '2']
);
});
assert.equal(
await client.ft.synUpdate('index', 'groupId', 'term'),
'OK'
);
}, GLOBAL.SERVERS.OPEN);
it('with SKIPINITIALSCAN', () => {
assert.deepEqual(
SYNUPDATE.transformArguments('index', 'groupId', 'term', {
SKIPINITIALSCAN: true
}),
['FT.SYNUPDATE', 'index', 'groupId', 'SKIPINITIALSCAN', 'term']
);
});
});
testUtils.testWithClient('client.ft.synUpdate', async client => {
const [, reply] = await Promise.all([
client.ft.create('index', {
field: SCHEMA_FIELD_TYPE.TEXT
}),
client.ft.synUpdate('index', 'groupId', 'term')
]);
assert.equal(reply, 'OK');
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,23 +1,26 @@
import { pushVerdictArguments } from '@redis/client/dist/lib/commands/generic-transformers';
import { RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { SimpleStringReply, Command, RedisArgument } from '@redis/client/dist/lib/RESP/types';
import { RedisVariadicArgument, pushVariadicArguments } from '@redis/client/dist/lib/commands/generic-transformers';
interface SynUpdateOptions {
SKIPINITIALSCAN?: true;
export interface FtSynUpdateOptions {
SKIPINITIALSCAN?: boolean;
}
export function transformArguments(
index: string,
groupId: string,
terms: string | Array<string>,
options?: SynUpdateOptions
): RedisCommandArguments {
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(
index: RedisArgument,
groupId: RedisArgument,
terms: RedisVariadicArgument,
options?: FtSynUpdateOptions
) {
const args = ['FT.SYNUPDATE', index, groupId];
if (options?.SKIPINITIALSCAN) {
args.push('SKIPINITIALSCAN');
args.push('SKIPINITIALSCAN');
}
return pushVerdictArguments(args, terms);
}
export declare function transformReply(): 'OK';
return pushVariadicArguments(args, terms);
},
transformReply: undefined as unknown as () => SimpleStringReply<'OK'>
} as const satisfies Command;

View File

@@ -1,24 +1,24 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { SchemaFieldTypes } from '.';
import { transformArguments } from './TAGVALS';
import TAGVALS from './TAGVALS';
import { SCHEMA_FIELD_TYPE } from './CREATE';
describe('TAGVALS', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments('index', '@field'),
['FT.TAGVALS', 'index', '@field']
);
});
describe('FT.TAGVALS', () => {
it('transformArguments', () => {
assert.deepEqual(
TAGVALS.transformArguments('index', '@field'),
['FT.TAGVALS', 'index', '@field']
);
});
testUtils.testWithClient('client.ft.tagVals', async client => {
await client.ft.create('index', {
field: SchemaFieldTypes.TAG
});
testUtils.testWithClient('client.ft.tagVals', async client => {
const [, reply] = await Promise.all([
client.ft.create('index', {
field: SCHEMA_FIELD_TYPE.TAG
}),
client.ft.tagVals('index', 'field')
]);
assert.deepEqual(
await client.ft.tagVals('index', 'field'),
[]
);
}, GLOBAL.SERVERS.OPEN);
assert.deepEqual(reply, []);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,5 +1,13 @@
export function transformArguments(index: string, fieldName: string): Array<string> {
return ['FT.TAGVALS', index, fieldName];
}
import { RedisArgument, ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
export declare function transformReply(): Array<string>;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments(index: RedisArgument, fieldName: RedisArgument) {
return ['FT.TAGVALS', index, fieldName];
},
transformReply: {
2: undefined as unknown as () => ArrayReply<BlobStringReply>,
3: undefined as unknown as () => SetReply<BlobStringReply>
}
} as const satisfies Command;

View File

@@ -1,19 +1,19 @@
import { strict as assert } from 'assert';
import { strict as assert } from 'node:assert';
import testUtils, { GLOBAL } from '../test-utils';
import { transformArguments } from './_LIST';
import _LIST from './_LIST';
describe('_LIST', () => {
it('transformArguments', () => {
assert.deepEqual(
transformArguments(),
['FT._LIST']
);
});
it('transformArguments', () => {
assert.deepEqual(
_LIST.transformArguments(),
['FT._LIST']
);
});
testUtils.testWithClient('client.ft._list', async client => {
assert.deepEqual(
await client.ft._list(),
[]
);
}, GLOBAL.SERVERS.OPEN);
testUtils.testWithClient('client.ft._list', async client => {
assert.deepEqual(
await client.ft._list(),
[]
);
}, GLOBAL.SERVERS.OPEN);
});

View File

@@ -1,5 +1,13 @@
export function transformArguments(): Array<string> {
return ['FT._LIST'];
}
import { ArrayReply, SetReply, BlobStringReply, Command } from '@redis/client/dist/lib/RESP/types';
export declare function transformReply(): Array<string>;
export default {
FIRST_KEY_INDEX: undefined,
IS_READ_ONLY: true,
transformArguments() {
return ['FT._LIST'];
},
transformReply: {
2: undefined as unknown as () => ArrayReply<BlobStringReply>,
3: undefined as unknown as () => SetReply<BlobStringReply>
}
} as const satisfies Command;

View File

@@ -1,5 +1,6 @@
import { strict as assert } from 'assert';
import { pushArgumentsWithLength, pushSortByArguments } from '.';
import { strict as assert } from 'node:assert';
/* import { pushArgumentsWithLength, pushSortByArguments } from '.';
describe('pushSortByArguments', () => {
describe('single', () => {
@@ -44,3 +45,4 @@ it('pushArgumentsWithLength', () => {
['a', '2', 'b', 'c']
);
});
*/

View File

@@ -1,690 +1,105 @@
import * as _LIST from './_LIST';
import * as ALTER from './ALTER';
import * as AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR';
import * as AGGREGATE from './AGGREGATE';
import * as ALIASADD from './ALIASADD';
import * as ALIASDEL from './ALIASDEL';
import * as ALIASUPDATE from './ALIASUPDATE';
import * as CONFIG_GET from './CONFIG_GET';
import * as CONFIG_SET from './CONFIG_SET';
import * as CREATE from './CREATE';
import * as CURSOR_DEL from './CURSOR_DEL';
import * as CURSOR_READ from './CURSOR_READ';
import * as DICTADD from './DICTADD';
import * as DICTDEL from './DICTDEL';
import * as DICTDUMP from './DICTDUMP';
import * as DROPINDEX from './DROPINDEX';
import * as EXPLAIN from './EXPLAIN';
import * as EXPLAINCLI from './EXPLAINCLI';
import * as INFO from './INFO';
import * as PROFILESEARCH from './PROFILE_SEARCH';
import * as PROFILEAGGREGATE from './PROFILE_AGGREGATE';
import * as SEARCH from './SEARCH';
import * as SEARCH_NOCONTENT from './SEARCH_NOCONTENT';
import * as SPELLCHECK from './SPELLCHECK';
import * as SUGADD from './SUGADD';
import * as SUGDEL from './SUGDEL';
import * as SUGGET_WITHPAYLOADS from './SUGGET_WITHPAYLOADS';
import * as SUGGET_WITHSCORES_WITHPAYLOADS from './SUGGET_WITHSCORES_WITHPAYLOADS';
import * as SUGGET_WITHSCORES from './SUGGET_WITHSCORES';
import * as SUGGET from './SUGGET';
import * as SUGLEN from './SUGLEN';
import * as SYNDUMP from './SYNDUMP';
import * as SYNUPDATE from './SYNUPDATE';
import * as TAGVALS from './TAGVALS';
import { RedisCommandArgument, RedisCommandArguments } from '@redis/client/dist/lib/commands';
import { pushOptionalVerdictArgument, pushVerdictArgument } from '@redis/client/dist/lib/commands/generic-transformers';
import { SearchOptions } from './SEARCH';
import _LIST from './_LIST';
import ALTER from './ALTER';
import AGGREGATE_WITHCURSOR from './AGGREGATE_WITHCURSOR';
import AGGREGATE from './AGGREGATE';
import ALIASADD from './ALIASADD';
import ALIASDEL from './ALIASDEL';
import ALIASUPDATE from './ALIASUPDATE';
import CONFIG_GET from './CONFIG_GET';
import CONFIG_SET from './CONFIG_SET';
import CREATE from './CREATE';
import CURSOR_DEL from './CURSOR_DEL';
import CURSOR_READ from './CURSOR_READ';
import DICTADD from './DICTADD';
import DICTDEL from './DICTDEL';
import DICTDUMP from './DICTDUMP';
import DROPINDEX from './DROPINDEX';
import EXPLAIN from './EXPLAIN';
import EXPLAINCLI from './EXPLAINCLI';
import INFO from './INFO';
import PROFILESEARCH from './PROFILE_SEARCH';
import PROFILEAGGREGATE from './PROFILE_AGGREGATE';
import SEARCH_NOCONTENT from './SEARCH_NOCONTENT';
import SEARCH from './SEARCH';
import SPELLCHECK from './SPELLCHECK';
import SUGADD from './SUGADD';
import SUGDEL from './SUGDEL';
import SUGGET_WITHPAYLOADS from './SUGGET_WITHPAYLOADS';
import SUGGET_WITHSCORES_WITHPAYLOADS from './SUGGET_WITHSCORES_WITHPAYLOADS';
import SUGGET_WITHSCORES from './SUGGET_WITHSCORES';
import SUGGET from './SUGGET';
import SUGLEN from './SUGLEN';
import SYNDUMP from './SYNDUMP';
import SYNUPDATE from './SYNUPDATE';
import TAGVALS from './TAGVALS';
export default {
_LIST,
_list: _LIST,
ALTER,
alter: ALTER,
AGGREGATE_WITHCURSOR,
aggregateWithCursor: AGGREGATE_WITHCURSOR,
AGGREGATE,
aggregate: AGGREGATE,
ALIASADD,
aliasAdd: ALIASADD,
ALIASDEL,
aliasDel: ALIASDEL,
ALIASUPDATE,
aliasUpdate: ALIASUPDATE,
CONFIG_GET,
configGet: CONFIG_GET,
CONFIG_SET,
configSet: CONFIG_SET,
CREATE,
create: CREATE,
CURSOR_DEL,
cursorDel: CURSOR_DEL,
CURSOR_READ,
cursorRead: CURSOR_READ,
DICTADD,
dictAdd: DICTADD,
DICTDEL,
dictDel: DICTDEL,
DICTDUMP,
dictDump: DICTDUMP,
DROPINDEX,
dropIndex: DROPINDEX,
EXPLAIN,
explain: EXPLAIN,
EXPLAINCLI,
explainCli: EXPLAINCLI,
INFO,
info: INFO,
PROFILESEARCH,
profileSearch: PROFILESEARCH,
PROFILEAGGREGATE,
profileAggregate: PROFILEAGGREGATE,
SEARCH,
search: SEARCH,
SEARCH_NOCONTENT,
searchNoContent: SEARCH_NOCONTENT,
SPELLCHECK,
spellCheck: SPELLCHECK,
SUGADD,
sugAdd: SUGADD,
SUGDEL,
sugDel: SUGDEL,
SUGGET_WITHPAYLOADS,
sugGetWithPayloads: SUGGET_WITHPAYLOADS,
SUGGET_WITHSCORES_WITHPAYLOADS,
sugGetWithScoresWithPayloads: SUGGET_WITHSCORES_WITHPAYLOADS,
SUGGET_WITHSCORES,
sugGetWithScores: SUGGET_WITHSCORES,
SUGGET,
sugGet: SUGGET,
SUGLEN,
sugLen: SUGLEN,
SYNDUMP,
synDump: SYNDUMP,
SYNUPDATE,
synUpdate: SYNUPDATE,
TAGVALS,
tagVals: TAGVALS
_LIST,
_list: _LIST,
ALTER,
alter: ALTER,
AGGREGATE_WITHCURSOR,
aggregateWithCursor: AGGREGATE_WITHCURSOR,
AGGREGATE,
aggregate: AGGREGATE,
ALIASADD,
aliasAdd: ALIASADD,
ALIASDEL,
aliasDel: ALIASDEL,
ALIASUPDATE,
aliasUpdate: ALIASUPDATE,
CONFIG_GET,
configGet: CONFIG_GET,
CONFIG_SET,
configSet: CONFIG_SET,
CREATE,
create: CREATE,
CURSOR_DEL,
cursorDel: CURSOR_DEL,
CURSOR_READ,
cursorRead: CURSOR_READ,
DICTADD,
dictAdd: DICTADD,
DICTDEL,
dictDel: DICTDEL,
DICTDUMP,
dictDump: DICTDUMP,
DROPINDEX,
dropIndex: DROPINDEX,
EXPLAIN,
explain: EXPLAIN,
EXPLAINCLI,
explainCli: EXPLAINCLI,
INFO,
info: INFO,
PROFILESEARCH,
profileSearch: PROFILESEARCH,
PROFILEAGGREGATE,
profileAggregate: PROFILEAGGREGATE,
SEARCH_NOCONTENT,
searchNoContent: SEARCH_NOCONTENT,
SEARCH,
search: SEARCH,
SPELLCHECK,
spellCheck: SPELLCHECK,
SUGADD,
sugAdd: SUGADD,
SUGDEL,
sugDel: SUGDEL,
SUGGET_WITHPAYLOADS,
sugGetWithPayloads: SUGGET_WITHPAYLOADS,
SUGGET_WITHSCORES_WITHPAYLOADS,
sugGetWithScoresWithPayloads: SUGGET_WITHSCORES_WITHPAYLOADS,
SUGGET_WITHSCORES,
sugGetWithScores: SUGGET_WITHSCORES,
SUGGET,
sugGet: SUGGET,
SUGLEN,
sugLen: SUGLEN,
SYNDUMP,
synDump: SYNDUMP,
SYNUPDATE,
synUpdate: SYNUPDATE,
TAGVALS,
tagVals: TAGVALS
};
export enum RedisSearchLanguages {
ARABIC = 'Arabic',
BASQUE = 'Basque',
CATALANA = 'Catalan',
DANISH = 'Danish',
DUTCH = 'Dutch',
ENGLISH = 'English',
FINNISH = 'Finnish',
FRENCH = 'French',
GERMAN = 'German',
GREEK = 'Greek',
HUNGARIAN = 'Hungarian',
INDONESAIN = 'Indonesian',
IRISH = 'Irish',
ITALIAN = 'Italian',
LITHUANIAN = 'Lithuanian',
NEPALI = 'Nepali',
NORWEIGAN = 'Norwegian',
PORTUGUESE = 'Portuguese',
ROMANIAN = 'Romanian',
RUSSIAN = 'Russian',
SPANISH = 'Spanish',
SWEDISH = 'Swedish',
TAMIL = 'Tamil',
TURKISH = 'Turkish',
CHINESE = 'Chinese'
}
export type PropertyName = `${'@' | '$.'}${string}`;
export type SortByProperty = string | {
BY: string;
DIRECTION?: 'ASC' | 'DESC';
};
export function pushSortByProperty(args: RedisCommandArguments, sortBy: SortByProperty): void {
if (typeof sortBy === 'string') {
args.push(sortBy);
} else {
args.push(sortBy.BY);
if (sortBy.DIRECTION) {
args.push(sortBy.DIRECTION);
}
}
}
export function pushSortByArguments(args: RedisCommandArguments, name: string, sortBy: SortByProperty | Array<SortByProperty>): RedisCommandArguments {
const lengthBefore = args.push(
name,
'' // will be overwritten
);
if (Array.isArray(sortBy)) {
for (const field of sortBy) {
pushSortByProperty(args, field);
}
} else {
pushSortByProperty(args, sortBy);
}
args[lengthBefore - 1] = (args.length - lengthBefore).toString();
return args;
}
export function pushArgumentsWithLength(args: RedisCommandArguments, fn: (args: RedisCommandArguments) => void): RedisCommandArguments {
const lengthIndex = args.push('') - 1;
fn(args);
args[lengthIndex] = (args.length - lengthIndex - 1).toString();
return args;
}
export enum SchemaFieldTypes {
TEXT = 'TEXT',
NUMERIC = 'NUMERIC',
GEO = 'GEO',
TAG = 'TAG',
VECTOR = 'VECTOR',
GEOSHAPE = 'GEOSHAPE'
}
type CreateSchemaField<
T extends SchemaFieldTypes,
E = Record<PropertyKey, unknown>
> = T | ({
type: T;
AS?: string;
INDEXMISSING?: boolean;
} & E);
type CommonFieldArguments = {
SORTABLE?: boolean | 'UNF';
NOINDEX?: boolean;
};
type CreateSchemaCommonField<
T extends SchemaFieldTypes,
E = Record<PropertyKey, unknown>
> = CreateSchemaField<
T,
(CommonFieldArguments & E)
>;
function pushCommonFieldArguments(args: RedisCommandArguments, fieldOptions: CommonFieldArguments) {
if (fieldOptions.SORTABLE) {
args.push('SORTABLE');
if (fieldOptions.SORTABLE === 'UNF') {
args.push('UNF');
}
}
if (fieldOptions.NOINDEX) {
args.push('NOINDEX');
}
}
export enum SchemaTextFieldPhonetics {
DM_EN = 'dm:en',
DM_FR = 'dm:fr',
FM_PT = 'dm:pt',
DM_ES = 'dm:es'
}
type CreateSchemaTextField = CreateSchemaCommonField<SchemaFieldTypes.TEXT, {
NOSTEM?: true;
WEIGHT?: number;
PHONETIC?: SchemaTextFieldPhonetics;
WITHSUFFIXTRIE?: boolean;
INDEXEMPTY?: boolean;
}>;
type CreateSchemaNumericField = CreateSchemaCommonField<SchemaFieldTypes.NUMERIC>;
type CreateSchemaGeoField = CreateSchemaCommonField<SchemaFieldTypes.GEO>;
type CreateSchemaTagField = CreateSchemaCommonField<SchemaFieldTypes.TAG, {
SEPARATOR?: string;
CASESENSITIVE?: true;
WITHSUFFIXTRIE?: boolean;
INDEXEMPTY?: boolean;
}>;
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 const SCHEMA_GEO_SHAPE_COORD_SYSTEM = {
SPHERICAL: 'SPHERICAL',
FLAT: 'FLAT'
} as const;
export type SchemaGeoShapeFieldCoordSystem = typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM[keyof typeof SCHEMA_GEO_SHAPE_COORD_SYSTEM];
type CreateSchemaGeoShapeField = CreateSchemaCommonField<SchemaFieldTypes.GEOSHAPE, {
COORD_SYSTEM?: SchemaGeoShapeFieldCoordSystem;
}>;
export interface RediSearchSchema {
[field: string]:
CreateSchemaTextField |
CreateSchemaNumericField |
CreateSchemaGeoField |
CreateSchemaTagField |
CreateSchemaFlatVectorField |
CreateSchemaHNSWVectorField |
CreateSchemaGeoShapeField;
}
export function pushSchema(args: RedisCommandArguments, schema: RediSearchSchema) {
for (const [field, fieldOptions] of Object.entries(schema)) {
args.push(field);
if (typeof fieldOptions === 'string') {
args.push(fieldOptions);
continue;
}
if (fieldOptions.AS) {
args.push('AS', fieldOptions.AS);
}
args.push(fieldOptions.type);
switch (fieldOptions.type) {
case SchemaFieldTypes.TEXT:
if (fieldOptions.NOSTEM) {
args.push('NOSTEM');
}
if (fieldOptions.WEIGHT) {
args.push('WEIGHT', fieldOptions.WEIGHT.toString());
}
if (fieldOptions.PHONETIC) {
args.push('PHONETIC', fieldOptions.PHONETIC);
}
if (fieldOptions.WITHSUFFIXTRIE) {
args.push('WITHSUFFIXTRIE');
}
pushCommonFieldArguments(args, fieldOptions);
if (fieldOptions.INDEXEMPTY) {
args.push('INDEXEMPTY');
}
break;
case SchemaFieldTypes.NUMERIC:
case SchemaFieldTypes.GEO:
pushCommonFieldArguments(args, fieldOptions);
break;
case SchemaFieldTypes.TAG:
if (fieldOptions.SEPARATOR) {
args.push('SEPARATOR', fieldOptions.SEPARATOR);
}
if (fieldOptions.CASESENSITIVE) {
args.push('CASESENSITIVE');
}
if (fieldOptions.WITHSUFFIXTRIE) {
args.push('WITHSUFFIXTRIE');
}
pushCommonFieldArguments(args, fieldOptions);
if (fieldOptions.INDEXEMPTY) {
args.push('INDEXEMPTY');
}
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;
}
});
break;
case SchemaFieldTypes.GEOSHAPE:
if (fieldOptions.COORD_SYSTEM !== undefined) {
args.push('COORD_SYSTEM', fieldOptions.COORD_SYSTEM);
}
pushCommonFieldArguments(args, fieldOptions);
break;
}
if (fieldOptions.INDEXMISSING) {
args.push('INDEXMISSING');
}
}
}
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, typeof value === 'number' ? value.toString() : value);
}
}
return args;
}
export function pushSearchOptions(
args: RedisCommandArguments,
options?: SearchOptions
): RedisCommandArguments {
if (options?.VERBATIM) {
args.push('VERBATIM');
}
if (options?.NOSTOPWORDS) {
args.push('NOSTOPWORDS');
}
// if (options?.WITHSCORES) {
// args.push('WITHSCORES');
// }
// if (options?.WITHPAYLOADS) {
// args.push('WITHPAYLOADS');
// }
pushOptionalVerdictArgument(args, 'INKEYS', options?.INKEYS);
pushOptionalVerdictArgument(args, 'INFIELDS', options?.INFIELDS);
pushOptionalVerdictArgument(args, 'RETURN', options?.RETURN);
if (options?.SUMMARIZE) {
args.push('SUMMARIZE');
if (typeof options.SUMMARIZE === 'object') {
if (options.SUMMARIZE.FIELDS) {
args.push('FIELDS');
pushVerdictArgument(args, options.SUMMARIZE.FIELDS);
}
if (options.SUMMARIZE.FRAGS) {
args.push('FRAGS', options.SUMMARIZE.FRAGS.toString());
}
if (options.SUMMARIZE.LEN) {
args.push('LEN', options.SUMMARIZE.LEN.toString());
}
if (options.SUMMARIZE.SEPARATOR) {
args.push('SEPARATOR', options.SUMMARIZE.SEPARATOR);
}
}
}
if (options?.HIGHLIGHT) {
args.push('HIGHLIGHT');
if (typeof options.HIGHLIGHT === 'object') {
if (options.HIGHLIGHT.FIELDS) {
args.push('FIELDS');
pushVerdictArgument(args, options.HIGHLIGHT.FIELDS);
}
if (options.HIGHLIGHT.TAGS) {
args.push('TAGS', options.HIGHLIGHT.TAGS.open, options.HIGHLIGHT.TAGS.close);
}
}
}
if (options?.SLOP) {
args.push('SLOP', options.SLOP.toString());
}
if (options?.INORDER) {
args.push('INORDER');
}
if (options?.LANGUAGE) {
args.push('LANGUAGE', options.LANGUAGE);
}
if (options?.EXPANDER) {
args.push('EXPANDER', options.EXPANDER);
}
if (options?.SCORER) {
args.push('SCORER', options.SCORER);
}
// if (options?.EXPLAINSCORE) {
// args.push('EXPLAINSCORE');
// }
// if (options?.PAYLOAD) {
// args.push('PAYLOAD', options.PAYLOAD);
// }
if (options?.SORTBY) {
args.push('SORTBY');
pushSortByProperty(args, options.SORTBY);
}
// if (options?.MSORTBY) {
// pushSortByArguments(args, 'MSORTBY', options.MSORTBY);
// }
if (options?.LIMIT) {
args.push(
'LIMIT',
options.LIMIT.from.toString(),
options.LIMIT.size.toString()
);
}
if (options?.PARAMS) {
pushParamsArgs(args, options.PARAMS);
}
if (options?.DIALECT) {
args.push('DIALECT', options.DIALECT.toString());
}
if (options?.RETURN?.length === 0) {
args.preserve = true;
}
if (options?.TIMEOUT !== undefined) {
args.push('TIMEOUT', options.TIMEOUT.toString());
}
return args;
}
interface SearchDocumentValue {
[key: string]: string | number | null | Array<SearchDocumentValue> | SearchDocumentValue;
}
export interface SearchReply {
total: number;
documents: Array<{
id: string;
value: SearchDocumentValue;
}>;
}
export interface ProfileOptions {
LIMITED?: true;
}
export type ProfileRawReply<T> = [
results: T,
profile: [
_: string,
TotalProfileTime: string,
_: string,
ParsingTime: string,
_: string,
PipelineCreationTime: string,
_: string,
IteratorsProfile: Array<any>
]
];
export interface ProfileReply {
results: SearchReply | AGGREGATE.AggregateReply;
profile: ProfileData;
}
interface ChildIterator {
type?: string,
counter?: number,
term?: string,
size?: number,
time?: string,
childIterators?: Array<ChildIterator>
}
interface IteratorsProfile {
type?: string,
counter?: number,
queryType?: string,
time?: string,
childIterators?: Array<ChildIterator>
}
interface ProfileData {
totalProfileTime: string,
parsingTime: string,
pipelineCreationTime: string,
iteratorsProfile: IteratorsProfile
}
export function transformProfile(reply: Array<any>): ProfileData{
return {
totalProfileTime: reply[0][1],
parsingTime: reply[1][1],
pipelineCreationTime: reply[2][1],
iteratorsProfile: transformIterators(reply[3][1])
};
}
function transformIterators(IteratorsProfile: Array<any>): IteratorsProfile {
var res: IteratorsProfile = {};
for (let i = 0; i < IteratorsProfile.length; i += 2) {
const value = IteratorsProfile[i+1];
switch (IteratorsProfile[i]) {
case 'Type':
res.type = value;
break;
case 'Counter':
res.counter = value;
break;
case 'Time':
res.time = value;
break;
case 'Query type':
res.queryType = value;
break;
case 'Child iterators':
res.childIterators = value.map(transformChildIterators);
break;
}
}
return res;
}
function transformChildIterators(IteratorsProfile: Array<any>): ChildIterator {
var res: ChildIterator = {};
for (let i = 1; i < IteratorsProfile.length; i += 2) {
const value = IteratorsProfile[i+1];
switch (IteratorsProfile[i]) {
case 'Type':
res.type = value;
break;
case 'Counter':
res.counter = value;
break;
case 'Time':
res.time = value;
break;
case 'Size':
res.size = value;
break;
case 'Term':
res.term = value;
break;
case 'Child iterators':
res.childIterators = value.map(transformChildIterators);
break;
}
}
return res;
}