Skip to content

Commit e9d4b94

Browse files
committed
Merge branch 'main' into fe/feature/RI-3804_optimize-ls
# Conflicts: # redisinsight/ui/src/slices/app/context.ts
2 parents 588e10c + 899528b commit e9d4b94

File tree

35 files changed

+355
-77
lines changed

35 files changed

+355
-77
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,10 @@ renderer.prod.js.map
4646
redisinsight/ui/style.css
4747
redisinsight/ui/style.css.map
4848
redisinsight/ui/dist
49+
redisinsight/api/commands
50+
redisinsight/api/guides
51+
redisinsight/api/tutorials
52+
redisinsight/api/content
4953
dist
5054
distWeb
5155
dll

redisinsight/api/src/constants/error-messages.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,4 +52,5 @@ export default {
5252
REDIS_MODULE_IS_REQUIRED: (module: string) => `Required ${module} module is not loaded.`,
5353
APP_SETTINGS_NOT_FOUND: () => 'Could not find application settings.',
5454
SERVER_INFO_NOT_FOUND: () => 'Could not find server info.',
55+
INCREASE_MINIMUM_LIMIT: (count: string) => `Set MAXSEARCHRESULTS to at least ${count}.`,
5556
};

redisinsight/api/src/constants/redis-error-codes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export enum RedisErrorCodes {
1313
BusyGroup = 'BUSYGROUP',
1414
NoGroup = 'NOGROUP',
1515
UnknownCommand = 'unknown command',
16+
RedisearchLimit = 'LIMIT',
1617
}
1718

1819
export enum CertificatesErrorCodes {

redisinsight/api/src/modules/browser/dto/keys.dto.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,4 +319,12 @@ export class GetKeysWithDetailsResponse {
319319
description: 'Node port. In case when we are working with cluster',
320320
})
321321
port?: number;
322+
323+
@ApiPropertyOptional({
324+
type: Number,
325+
description:
326+
'The maximum number of results.'
327+
+ ' For RediSearch this number is a value from "FT.CONFIG GET maxsearchresults" command.'
328+
})
329+
maxResults?: number;
322330
}

redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.spec.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -222,14 +222,15 @@ describe('RedisearchService', () => {
222222
cursor: mockSearchRedisearchDto.limit + mockSearchRedisearchDto.offset,
223223
scanned: 2,
224224
total: 100,
225+
maxResults: null,
225226
keys: [{
226227
name: keyName1,
227228
}, {
228229
name: keyName2,
229230
}],
230231
});
231232

232-
expect(nodeClient.sendCommand).toHaveBeenCalledTimes(1);
233+
expect(nodeClient.sendCommand).toHaveBeenCalledTimes(2);
233234
expect(nodeClient.sendCommand).toHaveBeenCalledWith(jasmine.objectContaining({
234235
name: 'FT.SEARCH',
235236
args: [
@@ -239,6 +240,13 @@ describe('RedisearchService', () => {
239240
'LIMIT', `${mockSearchRedisearchDto.offset}`, `${mockSearchRedisearchDto.limit}`,
240241
],
241242
}));
243+
expect(nodeClient.sendCommand).toHaveBeenCalledWith(jasmine.objectContaining( {
244+
name: 'FT.CONFIG',
245+
args: [
246+
'GET',
247+
'MAXSEARCHRESULTS',
248+
],
249+
}));
242250
});
243251
it('should search in cluster', async () => {
244252
browserTool.getRedisClient.mockResolvedValue(clusterClient);
@@ -252,13 +260,14 @@ describe('RedisearchService', () => {
252260
cursor: mockSearchRedisearchDto.limit + mockSearchRedisearchDto.offset,
253261
scanned: 2,
254262
total: 100,
263+
maxResults: null,
255264
keys: [
256265
{ name: keyName1 },
257266
{ name: keyName2 },
258267
],
259268
});
260269

261-
expect(clusterClient.sendCommand).toHaveBeenCalledTimes(1);
270+
expect(clusterClient.sendCommand).toHaveBeenCalledTimes(2);
262271
expect(clusterClient.sendCommand).toHaveBeenCalledWith(jasmine.objectContaining({
263272
name: 'FT.SEARCH',
264273
args: [
@@ -268,6 +277,13 @@ describe('RedisearchService', () => {
268277
'LIMIT', `${mockSearchRedisearchDto.offset}`, `${mockSearchRedisearchDto.limit}`,
269278
],
270279
}));
280+
expect(clusterClient.sendCommand).toHaveBeenCalledWith(jasmine.objectContaining( {
281+
name: 'FT.CONFIG',
282+
args: [
283+
'GET',
284+
'MAXSEARCHRESULTS',
285+
],
286+
}));
271287
});
272288
it('should handle ACL error (ft.info command)', async () => {
273289
when(nodeClient.sendCommand)

redisinsight/api/src/modules/browser/services/redisearch/redisearch.service.ts

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Cluster, Command, Redis } from 'ioredis';
2-
import { uniq } from 'lodash';
2+
import { toNumber, uniq } from 'lodash';
33
import {
4+
BadRequestException,
45
ConflictException,
56
Injectable,
67
Logger,
@@ -14,7 +15,9 @@ import {
1415
SearchRedisearchDto,
1516
} from 'src/modules/browser/dto/redisearch';
1617
import { GetKeysWithDetailsResponse } from 'src/modules/browser/dto';
18+
import { RedisErrorCodes } from 'src/constants';
1719
import { plainToClass } from 'class-transformer';
20+
import { numberWithSpaces } from 'src/utils/base.helper';
1821
import { BrowserToolService } from '../browser-tool/browser-tool.service';
1922

2023
@Injectable()
@@ -137,6 +140,7 @@ export class RedisearchService {
137140
this.logger.log('Searching keys using redisearch.');
138141

139142
try {
143+
let maxResults;
140144
const {
141145
index, query, offset, limit,
142146
} = dto;
@@ -147,16 +151,33 @@ export class RedisearchService {
147151
new Command('FT.SEARCH', [index, query, 'NOCONTENT', 'LIMIT', offset, limit]),
148152
);
149153

154+
155+
try {
156+
const [[, maxSearchResults]] = await client.sendCommand(
157+
// response: [ [ 'MAXSEARCHRESULTS', '10000' ] ]
158+
new Command('FT.CONFIG', ['GET', 'MAXSEARCHRESULTS'], {
159+
replyEncoding: 'utf8',
160+
}),
161+
) as [[string, string]];
162+
163+
maxResults = toNumber(maxSearchResults);
164+
} catch (error) {
165+
maxResults = null;
166+
}
167+
150168
return plainToClass(GetKeysWithDetailsResponse, {
151169
cursor: limit + offset,
152170
total,
153171
scanned: keyNames.length + offset,
154172
keys: keyNames.map((name) => ({ name })),
173+
maxResults,
155174
});
156-
} catch (e) {
157-
this.logger.error('Failed to search keys using redisearch index', e);
158-
159-
throw catchAclError(e);
175+
} catch (error) {
176+
this.logger.error('Failed to search keys using redisearch index', error);
177+
if (error.message?.includes(RedisErrorCodes.RedisearchLimit)) {
178+
throw new BadRequestException(ERROR_MESSAGES.INCREASE_MINIMUM_LIMIT(numberWithSpaces(dto.limit)));
179+
}
180+
throw catchAclError(error);
160181
}
161182
}
162183

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { numberWithSpaces } from 'src/utils/base.helper';
2+
3+
const numberWithSpacesTests = [
4+
{ input: 0, output: '0' },
5+
{ input: 10, output: '10' },
6+
{ input: 100, output: '100' },
7+
{ input: 1000, output: '1 000' },
8+
{ input: 1000.001, output: '1 000.001' },
9+
{ input: 5500, output: '5 500' },
10+
{ input: 1000000, output: '1 000 000' },
11+
{ input: 1233543234543243, output: '1 233 543 234 543 243' },
12+
{ input: NaN, output: 'NaN' },
13+
];
14+
15+
describe('numberWithSpaces', () => {
16+
numberWithSpacesTests.forEach((test) => {
17+
it(`should be output: ${test.output} for input: ${test.input} `, async () => {
18+
const result = numberWithSpaces(test.input);
19+
20+
expect(result).toEqual(test.output);
21+
});
22+
});
23+
});

redisinsight/api/src/utils/base.helper.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,7 @@ export const sortByNumberField = <T>(
44
items: T[],
55
field: string,
66
): T[] => sortBy(items, (o) => (o && isNumber(o[field]) ? o[field] : -Infinity));
7+
8+
9+
export const numberWithSpaces = (number: number = 0) =>
10+
number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ' ')

redisinsight/api/test/api/redisearch/POST-databases-id-redisearch-search.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { numberWithSpaces } from 'src/utils/base.helper';
12
import {
23
expect,
34
describe,
@@ -34,6 +35,7 @@ const responseSchema = Joi.object({
3435
cursor: Joi.number().integer().required(),
3536
scanned: Joi.number().integer().required(),
3637
total: Joi.number().integer().required(),
38+
maxResults: Joi.number().integer().allow(null).required(),
3739
keys: Joi.array().items(Joi.object({
3840
name: JoiRedisString.required(),
3941
})).required(),
@@ -62,6 +64,7 @@ describe('POST /databases/:id/redisearch/search', () => {
6264
expect(body.cursor).to.eq(10);
6365
expect(body.scanned).to.eq(10);
6466
expect(body.total).to.eq(2000);
67+
expect(body.maxResults).to.eq(10000);
6568
},
6669
},
6770
{
@@ -77,8 +80,21 @@ describe('POST /databases/:id/redisearch/search', () => {
7780
expect(body.cursor).to.eq(110);
7881
expect(body.scanned).to.eq(110);
7982
expect(body.total).to.eq(2000);
83+
expect(body.maxResults).to.eq(10000);
8084
},
8185
},
86+
{
87+
name: 'Should return custom error message if MAXSEARCHRESULTS less than request.limit',
88+
data: validInputData,
89+
statusCode: 400,
90+
responseBody: {
91+
statusCode: 400,
92+
error: 'Bad Request',
93+
message: `Set MAXSEARCHRESULTS to at least ${numberWithSpaces(validInputData.limit)}.`,
94+
},
95+
before: () => rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '1'),
96+
after: () => rte.data.setRedisearchConfig('MAXSEARCHRESULTS', '10000'),
97+
},
8298
].map(mainCheckFn);
8399
});
84100

@@ -107,6 +123,20 @@ describe('POST /databases/:id/redisearch/search', () => {
107123
},
108124
before: () => rte.data.setAclUserRules('~* +@all -ft.search')
109125
},
126+
{
127+
name: 'Should return response with maxResults = null if no permissions for "ft.config" command',
128+
endpoint: () => endpoint(constants.TEST_INSTANCE_ACL_ID),
129+
data: validInputData,
130+
responseSchema,
131+
checkFn: async ({ body }) => {
132+
expect(body.keys.length).to.eq(10);
133+
expect(body.cursor).to.eq(10);
134+
expect(body.scanned).to.eq(10);
135+
expect(body.total).to.eq(2000);
136+
expect(body.maxResults).to.eq(null);
137+
},
138+
before: () => rte.data.setAclUserRules('~* +@all -ft.config')
139+
},
110140
].map(mainCheckFn);
111141
});
112142
});

redisinsight/api/test/helpers/data/redis.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,15 @@ export const initDataHelper = (rte) => {
456456
}
457457
}
458458

459+
const setRedisearchConfig = async (
460+
rule: string,
461+
value: string,
462+
): Promise<any> => {
463+
const command = `FT.CONFIG SET ${rule} ${value}`;
464+
465+
return executeCommand(...command.split(' '));
466+
};
467+
459468
return {
460469
sendCommand,
461470
executeCommand,
@@ -477,5 +486,6 @@ export const initDataHelper = (rte) => {
477486
generateNStreams,
478487
generateNGraphs,
479488
getClientNodes,
489+
setRedisearchConfig,
480490
}
481491
}

0 commit comments

Comments
 (0)