Skip to content

Commit 82ab129

Browse files
authored
[ES|QL] Rerank command validation (#233691)
## Summary Part of #217285 We enable validation for the RERANK command Many validations come from parsing errors and are therefore difficult to read. - The fragment after ON should be standard for other commands and therefore no custom handlers. - WITH needs to be handled custom, like COMPLETION. - I have a custom handling for the query string value similar to COMPLETION. Note: For RERANK, our validation only works after the ON command is recognized, because the grammar requires it to define the input. Until the parser consumes On, the builder has no scope and we can see only syntax errors. <img width="1304" height="123" alt="Screenshot 2025-09-02 080759" src="https://github.com/user-attachments/assets/eb03e855-e957-41a0-8dff-4cec6e1ed9f0" /> <img width="916" height="332" alt="Screenshot 2025-09-02 080925" src="https://github.com/user-attachments/assets/859b97f9-054e-4c26-99f3-45ec8c3d8672" /> <img width="719" height="125" alt="Screenshot 2025-09-02 081011" src="https://github.com/user-attachments/assets/28f3ecc3-6d64-4850-b412-2939553faf3a" /> <img width="1301" height="122" alt="Screenshot 2025-09-02 081044" src="https://github.com/user-attachments/assets/145cb625-e9ad-418d-a0bf-fbb0d98b8b1d" />
1 parent 949359b commit 82ab129

File tree

11 files changed

+182
-64
lines changed

11 files changed

+182
-64
lines changed

src/platform/packages/shared/kbn-esql-ast/src/commands_registry/commands/completion/validate.test.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -104,19 +104,19 @@ describe('COMPLETION Validation', () => {
104104
it('prompt is a constant, but not text', async () => {
105105
await completionExpectErrors(
106106
`FROM index | COMPLETION 47 WITH { "inference_id": "inferenceId"}`,
107-
['[COMPLETION] prompt must be of type [text] but is [integer]']
107+
['COMPLETION query must be of type text. Found integer']
108108
);
109109

110110
await completionExpectErrors(
111111
`FROM index | COMPLETION true WITH { "inference_id": "inferenceId"}`,
112-
['[COMPLETION] prompt must be of type [text] but is [boolean]']
112+
['COMPLETION query must be of type text. Found boolean']
113113
);
114114
});
115115

116116
it('prompt is a function, but does not return text', async () => {
117117
await completionExpectErrors(
118118
`FROM index | COMPLETION PI() WITH { "inference_id": "inferenceId"}`,
119-
['[COMPLETION] prompt must be of type [text] but is [double]']
119+
['COMPLETION query must be of type text. Found double']
120120
);
121121

122122
await completionExpectErrors(
@@ -126,18 +126,18 @@ describe('COMPLETION Validation', () => {
126126
var0 == 0, 2,
127127
var0 == 1, 1
128128
) WITH { "inference_id": "inferenceId"}`,
129-
['[COMPLETION] prompt must be of type [text] but is [integer]']
129+
['COMPLETION query must be of type text. Found integer']
130130
);
131131

132132
await completionExpectErrors(
133133
`FROM index | COMPLETION TO_DATETIME("2023-12-02T11:00:00.000Z") WITH { "inference_id": "inferenceId"}`,
134-
['[COMPLETION] prompt must be of type [text] but is [date]']
134+
['COMPLETION query must be of type text. Found date']
135135
);
136136

137137
await completionExpectErrors(
138138
`FROM index | COMPLETION AVG(integerField) WITH { "inference_id": "inferenceId"}`,
139139
[
140-
'[COMPLETION] prompt must be of type [text] but is [double]',
140+
'COMPLETION query must be of type text. Found double',
141141
'Function AVG not allowed in COMPLETION',
142142
]
143143
);
@@ -146,19 +146,19 @@ describe('COMPLETION Validation', () => {
146146
it('prompt is a column, but not a text one', () => {
147147
completionExpectErrors(
148148
`FROM index | EVAL integerPrompt = 47 | COMPLETION integerPrompt WITH { "inference_id": "inferenceId"}`,
149-
['[COMPLETION] prompt must be of type [text] but is [integer]']
149+
['COMPLETION query must be of type text. Found integer']
150150
);
151151
completionExpectErrors(
152152
`FROM index | EVAL ipPrompt = to_ip("1.2.3.4") | COMPLETION ipPrompt WITH { "inference_id": "inferenceId"}`,
153-
['[COMPLETION] prompt must be of type [text] but is [ip]']
153+
['COMPLETION query must be of type text. Found ip']
154154
);
155155
completionExpectErrors(
156156
`FROM index | COMPLETION dateField WITH { "inference_id": "inferenceId"}`,
157-
['[COMPLETION] prompt must be of type [text] but is [date]']
157+
['COMPLETION query must be of type text. Found date']
158158
);
159159
completionExpectErrors(
160160
`FROM index | COMPLETION counterIntegerField WITH { "inference_id": "inferenceId"}`,
161-
['[COMPLETION] prompt must be of type [text] but is [counter_integer]']
161+
['COMPLETION query must be of type text. Found counter_integer']
162162
);
163163
});
164164

@@ -176,26 +176,26 @@ describe('COMPLETION Validation', () => {
176176
it('prompt is a parenthesized expression, but not a text one', () => {
177177
completionExpectErrors(
178178
`FROM index | COMPLETION (1 > 2) WITH { "inference_id": "inferenceId"}`,
179-
['[COMPLETION] prompt must be of type [text] but is [boolean]']
179+
['COMPLETION query must be of type text. Found boolean']
180180
);
181181
});
182182

183183
it('inference_id is not provided', () => {
184184
completionExpectErrors(`FROM index | COMPLETION "prompt"`, [
185-
'[COMPLETION] inference_id parameter is required',
185+
'"inference_id" parameter is required for COMPLETION.',
186186
]);
187187
completionExpectErrors(`FROM index | COMPLETION "prompt" WITH`, [
188-
'[COMPLETION] inference_id parameter is required',
188+
'"inference_id" parameter is required for COMPLETION.',
189189
]);
190190
completionExpectErrors(`FROM index | COMPLETION "prompt" WITH {}`, [
191-
'[COMPLETION] inference_id parameter is required',
191+
'"inference_id" parameter is required for COMPLETION.',
192192
]);
193193
completionExpectErrors(`FROM index | COMPLETION "prompt" WITH { "": ""}`, [
194-
'[COMPLETION] inference_id parameter is required',
194+
'"inference_id" parameter is required for COMPLETION.',
195195
]);
196196
completionExpectErrors(
197197
`FROM index | COMPLETION "prompt" WITH { "some_param": "some_value"}`,
198-
['[COMPLETION] inference_id parameter is required']
198+
['"inference_id" parameter is required for COMPLETION.']
199199
);
200200
});
201201
});

src/platform/packages/shared/kbn-esql-ast/src/commands_registry/commands/completion/validate.ts

Lines changed: 9 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@
66
* your election, the "Elastic License 2.0", the "GNU Affero General Public
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
9-
import { i18n } from '@kbn/i18n';
109
import type { ESQLAst, ESQLAstCompletionCommand, ESQLCommand, ESQLMessage } from '../../../types';
1110
import type { ICommandContext, ICommandCallbacks } from '../../types';
1211
import { getExpressionType } from '../../../definitions/utils/expressions';
1312
import { validateCommandArguments } from '../../../definitions/utils/validation';
13+
import { errors } from '../../../definitions/utils/errors';
1414

1515
const supportedPromptTypes = ['text', 'keyword', 'unknown', 'param'];
1616

@@ -31,27 +31,16 @@ export const validate = (
3131
);
3232

3333
if (!supportedPromptTypes.includes(promptExpressionType)) {
34-
messages.push({
35-
location: 'location' in prompt ? prompt?.location : location,
36-
text: i18n.translate('kbn-esql-ast.esql.validation.completionUnsupportedFieldType', {
37-
defaultMessage:
38-
'[COMPLETION] prompt must be of type [text] but is [{promptExpressionType}]',
39-
values: { promptExpressionType },
40-
}),
41-
type: 'error',
42-
code: 'completionUnsupportedFieldType',
43-
});
34+
messages.push(
35+
errors.byId('unsupportedQueryType', 'location' in prompt ? prompt?.location : location, {
36+
command: 'COMPLETION',
37+
expressionType: promptExpressionType,
38+
})
39+
);
4440
}
4541

46-
if (inferenceId.incomplete) {
47-
messages.push({
48-
location: command.location,
49-
text: i18n.translate('kbn-esql-ast.esql.validation.completionInferenceIdRequired', {
50-
defaultMessage: '[COMPLETION] inference_id parameter is required',
51-
}),
52-
type: 'error',
53-
code: 'completionInferenceIdRequired',
54-
});
42+
if (inferenceId?.incomplete) {
43+
messages.push(errors.byId('inferenceIdRequired', command.location, { command: 'COMPLETION' }));
5544
}
5645

5746
const targetName = targetField?.name || 'completion';
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the "Elastic License
4+
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
5+
* Public License v 1"; you may not use this file except in compliance with, at
6+
* your election, the "Elastic License 2.0", the "GNU Affero General Public
7+
* License v3.0 only", or the "Server Side Public License, v 1".
8+
*/
9+
10+
import { validate } from './validate';
11+
import { expectErrors } from '../../../__tests__/validation';
12+
import { mockContext } from '../../../__tests__/context_fixtures';
13+
14+
const rerankExpectErrors = (query: string, expected: string[]) =>
15+
expectErrors(query, expected, mockContext, 'rerank', validate);
16+
17+
describe('RERANK Validation', () => {
18+
describe('Query', () => {
19+
test('query as parameter', () => {
20+
rerankExpectErrors('FROM index | RERANK ?q ON keywordField', []);
21+
});
22+
23+
test('unsupported field type', () => {
24+
rerankExpectErrors('FROM index | RERANK col0=2 ON keywordField', [
25+
'RERANK query must be of type text. Found integer',
26+
]);
27+
});
28+
});
29+
30+
describe('Inference_id check', () => {
31+
test('inference_id as parameter', () => {
32+
rerankExpectErrors(
33+
'FROM index | RERANK "q" ON keywordField WITH { "inference_id": ?named_param }',
34+
[]
35+
);
36+
});
37+
38+
const msg = '"inference_id" parameter is required for RERANK.';
39+
40+
test('WITH without map (partial: WITH)', () => {
41+
rerankExpectErrors('FROM index | RERANK "q" ON keywordField WITH', [msg]);
42+
});
43+
44+
test('WITH with partial map start (WITH{)', () => {
45+
rerankExpectErrors('FROM index | RERANK "q" ON keywordField WITH{', [msg]);
46+
});
47+
48+
test('WITH with empty map (WITH {})', () => {
49+
rerankExpectErrors('FROM index | RERANK "q" ON keywordField WITH { }', [msg]);
50+
});
51+
52+
test('WITH map without inference_id key', () => {
53+
rerankExpectErrors('FROM index | RERANK "q" ON keywordField WITH { "some_param": "value" }', [
54+
msg,
55+
]);
56+
});
57+
58+
test('WITH inference_id with empty string value', () => {
59+
rerankExpectErrors('FROM index | RERANK "q" ON keywordField WITH { "inference_id": "" }', [
60+
msg,
61+
]);
62+
});
63+
});
64+
});

src/platform/packages/shared/kbn-esql-ast/src/commands_registry/commands/rerank/validate.ts

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
* License v3.0 only", or the "Server Side Public License, v 1".
88
*/
99

10-
import { i18n } from '@kbn/i18n';
1110
import type { ESQLCommand, ESQLMessage, ESQLAst, ESQLAstRerankCommand } from '../../../types';
1211
import type { ICommandContext, ICommandCallbacks } from '../../types';
12+
import { getExpressionType } from '../../../definitions/utils/expressions';
1313
import { validateCommandArguments } from '../../../definitions/utils/validation';
14+
import { errors } from '../../../definitions/utils/errors';
15+
16+
const supportedQueryTypes = ['keyword', 'text', 'param'];
1417

1518
export const validate = (
1619
command: ESQLCommand,
@@ -20,35 +23,39 @@ export const validate = (
2023
): ESQLMessage[] => {
2124
const messages: ESQLMessage[] = [];
2225

23-
// Run standard argument validation
24-
messages.push(...validateCommandArguments(command, ast, context, callbacks));
26+
const { query, targetField, location, inferenceId } = command as ESQLAstRerankCommand;
27+
const rerankExpressionType = getExpressionType(
28+
query,
29+
context?.fields,
30+
context?.userDefinedColumns
31+
);
2532

26-
// Cast to RERANK command for type-specific validation
27-
const rerankCommand = command as ESQLAstRerankCommand;
28-
29-
// Validate that query text is provided
30-
if (!rerankCommand.query) {
31-
messages.push({
32-
location: command.location,
33-
text: i18n.translate('kbn-esql-ast.esql.validation.rerankMissingQuery', {
34-
defaultMessage: '[RERANK] Query text is required.',
35-
}),
36-
type: 'error',
37-
code: 'rerankMissingQuery',
38-
});
33+
// check for supported query types
34+
if (!supportedQueryTypes.includes(rerankExpressionType)) {
35+
messages.push(
36+
errors.byId('unsupportedQueryType', 'location' in query ? query?.location : location, {
37+
command: 'RERANK',
38+
expressionType: rerankExpressionType,
39+
})
40+
);
3941
}
4042

41-
// Validate that at least one field is specified in ON clause
42-
if (!rerankCommand.fields || rerankCommand.fields.length === 0) {
43-
messages.push({
44-
location: command.location,
45-
text: i18n.translate('kbn-esql-ast.esql.validation.rerankMissingFields', {
46-
defaultMessage: '[RERANK] At least one field must be specified in the ON clause.',
47-
}),
48-
type: 'error',
49-
code: 'rerankMissingFields',
50-
});
43+
if (inferenceId?.incomplete) {
44+
messages.push(errors.byId('inferenceIdRequired', command.location, { command: 'RERANK' }));
5145
}
5246

47+
const targetName = targetField?.name || 'rerank';
48+
49+
// Sets the target field so the column is recognized after the command is applied
50+
context?.userDefinedColumns.set(targetName, [
51+
{
52+
name: targetName,
53+
location: targetField?.location || location,
54+
type: 'keyword',
55+
},
56+
]);
57+
58+
messages.push(...validateCommandArguments(command, ast, context, callbacks));
59+
5360
return messages;
5461
};

src/platform/packages/shared/kbn-esql-ast/src/commands_registry/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,11 @@ export enum Location {
213213
*/
214214
RENAME = 'rename',
215215

216+
/**
217+
* In the RERANK command
218+
*/
219+
RERANK = 'rerank',
220+
216221
/**
217222
* In the JOIN command (used only for AS)
218223
*/
@@ -244,6 +249,7 @@ const commandOptionNameToLocation: Record<string, Location> = {
244249
join: Location.JOIN,
245250
show: Location.SHOW,
246251
completion: Location.COMPLETION,
252+
rerank: Location.RERANK,
247253
};
248254

249255
/**

src/platform/packages/shared/kbn-esql-ast/src/definitions/all_operators.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const otherDefinitions: FunctionDefinition[] = [
9595
Location.DISSECT,
9696
Location.COMPLETION,
9797
Location.RENAME,
98+
Location.RERANK,
9899
],
99100
signatures: [
100101
{

src/platform/packages/shared/kbn-esql-ast/src/definitions/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,14 @@ export interface ValidationErrors {
356356
message: string;
357357
type: {};
358358
};
359+
inferenceIdRequired: {
360+
message: string;
361+
type: {};
362+
};
363+
unsupportedQueryType: {
364+
message: string;
365+
type: {};
366+
};
359367
}
360368

361369
export type ErrorTypes = keyof ValidationErrors;

src/platform/packages/shared/kbn-esql-ast/src/definitions/utils/errors.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,24 @@ Expected one of:
294294
}),
295295
type: 'warning',
296296
};
297+
298+
case 'inferenceIdRequired':
299+
return {
300+
message: i18n.translate('kbn-esql-ast.esql.validation.inferenceIdRequired', {
301+
defaultMessage: '"inference_id" parameter is required for {command}.',
302+
values: { command: out.command.toUpperCase() },
303+
}),
304+
type: 'error',
305+
};
306+
307+
case 'unsupportedQueryType':
308+
return {
309+
message: i18n.translate('kbn-esql-ast.esql.validation.unsupportedQueryType', {
310+
defaultMessage: '{command} query must be of type text. Found {expressionType}',
311+
values: { command: out.command.toUpperCase(), expressionType: out.expressionType },
312+
}),
313+
type: 'error',
314+
};
297315
}
298316
return { message: '' };
299317
}

src/platform/packages/shared/kbn-esql-ast/src/parser/__tests__/rerank.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ describe('RERANK', () => {
200200
fields: [{ type: 'column', name: 'title' }],
201201
});
202202

203-
expect(rerankCmd).not.toHaveProperty('inferenceId');
203+
expect(rerankCmd.inferenceId).toEqual(undefined);
204204
});
205205

206206
it('should handle missing ON clause', () => {

0 commit comments

Comments
 (0)