1
0
mirror of https://github.com/redis/node-redis.git synced 2025-08-07 13:22:56 +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

View File

@@ -1,6 +0,0 @@
.nyc_output/
coverage/
lib/
.nycrc.json
.release-it.json
tsconfig.json

View File

@@ -5,6 +5,7 @@
"tagAnnotation": "Release ${tagName}"
},
"npm": {
"versionArgs": ["--workspaces-update=false"],
"publishArgs": ["--access", "public"]
}
}

View File

@@ -1,18 +1,20 @@
# @redis/search
This package provides support for the [RediSearch](https://redisearch.io) module, which adds indexing and querying support for data stored in Redis Hashes or as JSON documents with the RedisJSON module. It extends the [Node Redis client](https://github.com/redis/node-redis) to include functions for each of the RediSearch commands.
This package provides support for the [RediSearch](https://redis.io/docs/interact/search-and-query/) module, which adds indexing and querying support for data stored in Redis Hashes or as JSON documents with the [RedisJSON](https://redis.io/docs/data-types/json/) module.
To use these extra commands, your Redis server must have the RediSearch module installed. To index and query JSON documents, you'll also need to add the RedisJSON module.
Should be used with [`redis`/`@redis/client`](https://github.com/redis/node-redis).
:warning: To use these extra commands, your Redis server must have the RediSearch module installed. To index and query JSON documents, you'll also need to add the RedisJSON module.
## Usage
For complete examples, see [`search-hashes.js`](https://github.com/redis/node-redis/blob/master/examples/search-hashes.js) and [`search-json.js`](https://github.com/redis/node-redis/blob/master/examples/search-json.js) in the Node Redis examples folder.
For complete examples, see [`search-hashes.js`](https://github.com/redis/node-redis/blob/master/examples/search-hashes.js) and [`search-json.js`](https://github.com/redis/node-redis/blob/master/examples/search-json.js) in the [examples folder](https://github.com/redis/node-redis/tree/master/examples).
### Indexing and Querying Data in Redis Hashes
#### Creating an Index
Before we can perform any searches, we need to tell RediSearch how to index our data, and which Redis keys to find that data in. The [FT.CREATE](https://redis.io/commands/ft.create) command creates a RediSearch index. Here's how to use it to create an index we'll call `idx:animals` where we want to index hashes containing `name`, `species` and `age` fields, and whose key names in Redis begin with the prefix `noderedis:animals`:
Before we can perform any searches, we need to tell RediSearch how to index our data, and which Redis keys to find that data in. The [FT.CREATE](https://redis.io/commands/ft.create) command creates a RediSearch index. Here's how to use it to create an index we'll call `idx:animals` where we want to index hashes containing `name`, `species` and `age` fields, and whose key names in Redis begin with the prefix `noderedis:animals`:
```javascript
await client.ft.create('idx:animals', {

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

View File

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

View File

@@ -2,15 +2,15 @@ import TestUtils from '@redis/test-utils';
import RediSearch from '.';
export default new TestUtils({
dockerImageName: 'redislabs/redisearch',
dockerImageName: 'redis/redis-stack',
dockerImageVersionArgument: 'redisearch-version',
defaultDockerVersion: '2.4.9'
defaultDockerVersion: '7.4.0-v1'
});
export const GLOBAL = {
SERVERS: {
OPEN: {
serverArguments: ['--loadmodule /usr/lib/redis/modules/redisearch.so'],
serverArguments: [],
clientOptions: {
modules: {
ft: RediSearch

View File

@@ -1,30 +1,24 @@
{
"name": "@redis/search",
"version": "1.2.0",
"version": "2.0.0-next.2",
"license": "MIT",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"main": "./dist/lib/index.js",
"types": "./dist/lib/index.d.ts",
"files": [
"dist/"
"dist/",
"!dist/tsconfig.tsbuildinfo"
],
"scripts": {
"test": "nyc -r text-summary -r lcov mocha -r source-map-support/register -r ts-node/register './lib/**/*.spec.ts'",
"build": "tsc",
"documentation": "typedoc"
"test": "nyc -r text-summary -r lcov mocha -r tsx './lib/**/*.spec.ts'"
},
"peerDependencies": {
"@redis/client": "^1.0.0"
"@redis/client": "^2.0.0-next.4"
},
"devDependencies": {
"@istanbuljs/nyc-config-typescript": "^1.0.2",
"@redis/test-utils": "*",
"@types/node": "^20.6.2",
"nyc": "^15.1.0",
"release-it": "^16.1.5",
"source-map-support": "^0.5.21",
"ts-node": "^10.9.1",
"typedoc": "^0.25.1",
"typescript": "^5.2.2"
"@redis/test-utils": "*"
},
"engines": {
"node": ">= 18"
},
"repository": {
"type": "git",