Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion packages/search/lib/commands/AGGREGATE.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,40 @@ describe('AGGREGATE', () => {
});
});

describe('transformReply', () => {
it('RESP2 reply has empty warnings', () => {
assert.deepEqual(
AGGREGATE.transformReply[2]([0]),
{ total: 0, results: [], warnings: [] }
);
});

it('RESP3 reply populates warnings from the `warning` field', () => {
const reply = new Map<string, unknown>([
['total_results', 0],
['results', []],
['warning', ['Timeout limit was reached']]
]);

assert.deepEqual(
AGGREGATE.transformReply[3](reply),
{ total: 0, results: [], warnings: ['Timeout limit was reached'] }
);
});

it('RESP3 reply without a `warning` field yields empty warnings', () => {
const reply = new Map<string, unknown>([
['total_results', 0],
['results', []]
]);

assert.deepEqual(
AGGREGATE.transformReply[3](reply),
{ total: 0, results: [], warnings: [] }
);
});
});

testUtils.testWithClient('client.ft.aggregate', async client => {
await client.ft.create('index', {
field: 'NUMERIC'
Expand Down Expand Up @@ -678,7 +712,8 @@ describe('AGGREGATE', () => {
enumerable: true
}
})
]
],
warnings: []
}
);
}, GLOBAL.SERVERS.OPEN);
Expand Down
15 changes: 12 additions & 3 deletions packages/search/lib/commands/AGGREGATE.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { RediSearchProperty } from './CREATE';
import { FtSearchParams, parseParamsArgument } from './SEARCH';
import { transformTuplesReply } from '@redis/client/dist/lib/commands/generic-transformers';
import { DEFAULT_DIALECT } from '../dialect/default';
import { getMapValue, mapLikeToFlatArray, mapLikeToObject, mapLikeValues, parseAggregateResultRow } from './reply-transformers';
import { getMapValue, mapLikeToFlatArray, mapLikeToObject, mapLikeValues, parseAggregateResultRow, parseWarnings } from './reply-transformers';
import { RESP_TYPES } from '@redis/client/dist/lib/RESP/decoder';

type LoadField = RediSearchProperty | {
Expand Down Expand Up @@ -149,6 +149,12 @@ export type AggregateRawReply = [
export interface AggregateReply {
total: number;
results: Array<MapReply<BlobStringReply, BlobStringReply>>;
/**
* Warnings returned alongside partial results (e.g. on query timeout under a
* `return` / `return-strict` on-timeout policy). Only populated on RESP3;
* always empty on RESP2.
*/
warnings: Array<string>;
};

function transformAggregateReplyResp2(
Expand All @@ -169,7 +175,9 @@ function transformAggregateReplyResp2(
// FT.AGGREGATE returns an array reply where each row is an array reply and represents a single aggregate result.
// The integer reply at position 1 does not represent a valid value.
total: Number(rawReply[0]),
results
results,
// FT.AGGREGATE only emits warnings on RESP3; RESP2 replies never carry them.
warnings: []
};
}

Expand Down Expand Up @@ -206,7 +214,8 @@ function transformAggregateReplyResp3(

return {
total,
results
results,
warnings: parseWarnings(reply)
};
}

Expand Down
1 change: 1 addition & 0 deletions packages/search/lib/commands/AGGREGATE_WITHCURSOR.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ describe('AGGREGATE WITHCURSOR', () => {
{
total: 0,
results: [],
warnings: [],
cursor: 0
}
);
Expand Down
3 changes: 2 additions & 1 deletion packages/search/lib/commands/CURSOR_READ.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ describe('FT.CURSOR READ', () => {
{
total: 0,
results: [],
cursor: 0
cursor: 0,
warnings: []
}
);
}, GLOBAL.SERVERS.OPEN);
Expand Down
19 changes: 2 additions & 17 deletions packages/search/lib/commands/HYBRID.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
mapLikeToObject,
mapLikeValues,
parseDocumentValue,
parseWarnings,
} from "./reply-transformers";

/**
Expand Down Expand Up @@ -443,9 +444,6 @@ function transformHybridSearchResults(reply: unknown): HybridSearchResult {
);

const rawResults = mapLikeValues(getMapValue(replyMap, ["results"]) ?? []);
const warnings = mapLikeValues(
getMapValue(replyMap, ["warnings", "warning"]) ?? [],
);

const executionTimeValue = getMapValue(replyMap, [
"execution_time",
Expand Down Expand Up @@ -493,24 +491,11 @@ function transformHybridSearchResults(reply: unknown): HybridSearchResult {
return {
totalResults,
executionTime,
warnings: warnings.map(toWarningString),
warnings: parseWarnings(replyMap),
results,
};
}

function parseReplyMap(reply: unknown): Record<string, unknown> {
return mapLikeToObject(reply);
}

function toWarningString(warning: unknown): string {
if (typeof warning === 'string') return warning;
if (warning instanceof Buffer) return warning.toString();
if (warning === null || warning === undefined) return '';
// Anything else (Map/Array/plain object) would collapse to "[object Object]"
// under a naive toString — JSON-serialize instead so the caller can read it.
try {
return JSON.stringify(warning);
} catch {
return String(warning);
}
}
40 changes: 38 additions & 2 deletions packages/search/lib/commands/SEARCH.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,40 @@ describe('FT.SEARCH', () => {
});
});

describe('transformReply', () => {
it('RESP2 reply has empty warnings', () => {
assert.deepEqual(
SEARCH.transformReply[2]([0]),
{ total: 0, documents: [], warnings: [] }
);
});

it('RESP3 reply populates warnings from the `warning` field', () => {
const reply = new Map<string, unknown>([
['total_results', 0],
['results', []],
['warning', ['Timeout limit was reached']]
]);

assert.deepEqual(
SEARCH.transformReply[3](reply),
{ total: 0, documents: [], warnings: ['Timeout limit was reached'] }
);
});

it('RESP3 reply without a `warning` field yields empty warnings', () => {
const reply = new Map<string, unknown>([
['total_results', 0],
['results', []]
]);

assert.deepEqual(
SEARCH.transformReply[3](reply),
{ total: 0, documents: [], warnings: [] }
);
});
});

describe('client.ft.search', () => {
testUtils.testWithClient('without optional options', async client => {
await Promise.all([
Expand All @@ -297,7 +331,8 @@ describe('FT.SEARCH', () => {
enumerable: true
}
})
}]
}],
warnings: []
}
);
}, GLOBAL.SERVERS.OPEN);
Expand All @@ -323,7 +358,8 @@ describe('FT.SEARCH', () => {
}, {
id: '2',
value: {}
}]
}],
warnings: []
}
);
}, GLOBAL.SERVERS.OPEN);
Expand Down
15 changes: 12 additions & 3 deletions packages/search/lib/commands/SEARCH.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { RedisArgument, Command, ReplyUnion, TypeMapping } from '@redis/client/d
import { RedisVariadicArgument, parseOptionalVariadicArgument } from '@redis/client/dist/lib/commands/generic-transformers';
import { RediSearchLanguage } from './CREATE';
import { DEFAULT_DIALECT } from '../dialect/default';
import { getMapValue, mapLikeToObject, mapLikeValues, parseDocumentValue, parseSearchResultRow } from './reply-transformers';
import { getMapValue, mapLikeToObject, mapLikeValues, parseDocumentValue, parseSearchResultRow, parseWarnings } from './reply-transformers';

export type FtSearchParams = Record<string, RedisArgument | number>;

Expand Down Expand Up @@ -179,7 +179,9 @@ function transformSearchReplyResp2(

return {
total: reply[0] as number,
documents
documents,
// FT.SEARCH only emits warnings on RESP3; RESP2 replies never carry them.
warnings: []
};
}

Expand Down Expand Up @@ -210,7 +212,8 @@ function transformSearchReplyResp3(

return {
total,
documents
documents,
warnings: parseWarnings(reply)
};
}

Expand Down Expand Up @@ -240,6 +243,12 @@ export interface SearchReply {
id: string;
value: SearchDocumentValue;
}>;
/**
* Warnings returned alongside partial results (e.g. on query timeout under a
* `return` / `return-strict` on-timeout policy). Only populated on RESP3;
* always empty on RESP2.
*/
warnings: Array<string>;
}

function documentValue(tuples: unknown) {
Expand Down
3 changes: 2 additions & 1 deletion packages/search/lib/commands/SEARCH_NOCONTENT.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ describe('FT.SEARCH NOCONTENT', () => {
await client.ft.searchNoContent('index', '*'),
{
total: 2,
documents: ['1', '2']
documents: ['1', '2'],
warnings: []
}
);
}, GLOBAL.SERVERS.OPEN);
Expand Down
14 changes: 12 additions & 2 deletions packages/search/lib/commands/SEARCH_NOCONTENT.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ export default {
2: (reply: SearchRawReply): SearchNoContentReply => {
return {
total: reply[0] as number,
documents: reply.slice(1) as Array<string>
documents: reply.slice(1) as Array<string>,
// FT.SEARCH only emits warnings on RESP3; RESP2 replies never carry them.
warnings: []
}
},
3: (
Expand All @@ -26,11 +28,13 @@ export default {
documents: Array<{
id: string;
}>;
warnings: Array<string>;
};

return {
total: transformed.total,
documents: transformed.documents.map(document => document.id)
documents: transformed.documents.map(document => document.id),
warnings: transformed.warnings
};
}
},
Expand All @@ -39,4 +43,10 @@ export default {
export interface SearchNoContentReply {
total: number;
documents: Array<string>;
/**
* Warnings returned alongside partial results (e.g. on query timeout under a
* `return` / `return-strict` on-timeout policy). Only populated on RESP3;
* always empty on RESP2.
*/
warnings: Array<string>;
};
24 changes: 24 additions & 0 deletions packages/search/lib/commands/reply-transformers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,30 @@ export function parseDocumentValue(value: unknown): Record<string, unknown> {
return document;
}

function toWarningString(warning: unknown): string {
if (typeof warning === 'string') return warning;
if (warning instanceof Buffer) return warning.toString();
if (warning === null || warning === undefined) return '';
// Anything else (Map/Array/plain object) would collapse to "[object Object]"
// under a naive toString — JSON-serialize instead so the caller can read it.
try {
return JSON.stringify(warning);
} catch {
return String(warning);
}
}

/**
* Extracts the `warning` field emitted by FT.SEARCH / FT.AGGREGATE / FT.HYBRID
* alongside partial results when a query hits its timeout under a `return` /
* `return-strict` on-timeout policy. Returns an empty array when absent (e.g.
* RESP2 replies for FT.SEARCH / FT.AGGREGATE, which never carry warnings).
*/
export function parseWarnings(replyMap: Record<string, unknown>): Array<string> {
const warnings = mapLikeValues(getMapValue(replyMap, ['warning', 'warnings']) ?? []);
return warnings.map(toWarningString);
}

function normalizeProfileValue(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(normalizeProfileValue);
Expand Down