Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -16595,6 +16595,12 @@ const textEmbeddingDefinition: FunctionDefinition = {
optional: false,
description:
'Identifier of an existing inference endpoint the that will generate the embeddings. The inference endpoint must have the `text_embedding` task type and should use the same model that was used to embed your indexed data.',
hint: {
entityType: 'inference_endpoint',
constraints: {
task_type: 'text_embedding',
},
},
},
],
returnType: 'dense_vector',
Expand All @@ -16614,8 +16620,6 @@ const textEmbeddingDefinition: FunctionDefinition = {
Location.JOIN,
],
examples: [
'ROW input="Who is Victor Hugo?"\n| EVAL embedding = TEXT_EMBEDDING("Who is Victor Hugo?", "test_dense_inference")',
'FROM dense_vector_text METADATA _score\n| EVAL query_embedding = TEXT_EMBEDDING("be excellent to each other", "test_dense_inference")\n| WHERE KNN(text_embedding_field, query_embedding)',
'FROM dense_vector_text METADATA _score\n| WHERE KNN(text_embedding_field, TEXT_EMBEDDING("be excellent to each other", "test_dense_inference"))',
],
};
Expand Down Expand Up @@ -19439,6 +19443,7 @@ const topSnippetsDefinition: FunctionDefinition = {
Location.JOIN,
],
examples: [
'FROM books\n| EVAL snippets = TOP_SNIPPETS(description, "Tolkien")',
'FROM books\n| WHERE MATCH(title, "Return")\n| EVAL snippets = TOP_SNIPPETS(description, "Tolkien", { "num_snippets": 3, "num_words": 25 })',
],
};
Expand Down Expand Up @@ -19786,7 +19791,6 @@ const vCosineDefinition: FunctionDefinition = {
description: i18n.translate('kbn-esql-ast.esql.definitions.v_cosine', {
defaultMessage: 'Calculates the cosine similarity between two dense_vectors.',
}),
ignoreAsSuggestion: true,
preview: true,
alias: undefined,
signatures: [
Expand Down Expand Up @@ -19833,7 +19837,6 @@ const vDotProductDefinition: FunctionDefinition = {
description: i18n.translate('kbn-esql-ast.esql.definitions.v_dot_product', {
defaultMessage: 'Calculates the dot product between two dense_vectors.',
}),
ignoreAsSuggestion: true,
preview: true,
alias: undefined,
signatures: [
Expand Down Expand Up @@ -19880,7 +19883,6 @@ const vHammingDefinition: FunctionDefinition = {
description: i18n.translate('kbn-esql-ast.esql.definitions.v_hamming', {
defaultMessage: 'Calculates the Hamming distance between two dense vectors.',
}),
ignoreAsSuggestion: true,
preview: true,
alias: undefined,
signatures: [
Expand Down Expand Up @@ -19927,7 +19929,6 @@ const vL1NormDefinition: FunctionDefinition = {
description: i18n.translate('kbn-esql-ast.esql.definitions.v_l1_norm', {
defaultMessage: 'Calculates the l1 norm between two dense_vectors.',
}),
ignoreAsSuggestion: true,
preview: true,
alias: undefined,
signatures: [
Expand Down Expand Up @@ -19974,7 +19975,6 @@ const vL2NormDefinition: FunctionDefinition = {
description: i18n.translate('kbn-esql-ast.esql.definitions.v_l2_norm', {
defaultMessage: 'Calculates the l2 norm between two dense_vectors.',
}),
ignoreAsSuggestion: true,
preview: true,
alias: undefined,
signatures: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const projectRouting = {
type: 'keyword',
serverlessOnly: true,
preview: true,
snapshotOnly: true,
snapshotOnly: false,
Comment thread
sddonne marked this conversation as resolved.
description:
'A project routing expression, used to define which projects to route the query to. Only supported if Cross-Project Search is enabled.',
ignoreAsSuggestion: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,13 @@ export const isParameterType = (str: string | undefined): str is FunctionParamet
export const isReturnType = (str: string | FunctionParameterType): str is FunctionReturnType =>
str !== 'unsupported' && (str === 'unknown' || str === 'any' || dataTypes.includes(str));

export const parameterHintEntityTypes = ['inference_endpoint'] as const;
export type ParameterHintEntityType = (typeof parameterHintEntityTypes)[number];
export interface ParameterHint {
entityType: ParameterHintEntityType;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's make it an enum instead

Copy link
Copy Markdown
Contributor Author

@sddonne sddonne Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a type that is used within the autogenerated files, we will have type errors in them if using enums.

constraints?: Record<string, string>;
}

export interface FunctionParameter {
name: string;
type: FunctionParameterType;
Expand Down Expand Up @@ -128,6 +135,12 @@ export interface FunctionParameter {
* This indicates that the parameter can accept multiple values, which will be passed as an array.
*/
supportsMultiValues?: boolean;

/**
* Provides information that is useful for getting parameter values from external sources.
* For example, an inference endpoint
*/
hint?: ParameterHint;
}

export interface ElasticsearchCommandDefinition {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
*/

import { ControlTriggerSource, ESQLVariableType } from '@kbn/esql-types';
import { uniq } from 'lodash';
import { isEqual, uniq, uniqWith } from 'lodash';
import { matchesSpecialFunction } from '../utils';
import { shouldSuggestComma, type CommaContext } from '../comma_decision_engine';
import type { ExpressionContext } from '../types';
Expand All @@ -21,6 +21,7 @@ import type {
FunctionDefinition,
FunctionParameter,
FunctionParameterType,
ParameterHint,
} from '../../../../types';
import { type ISuggestionItem } from '../../../../../registry/types';
import { FULL_TEXT_SEARCH_FUNCTIONS } from '../../../../constants';
Expand All @@ -29,6 +30,7 @@ import {
valuePlaceholderConstant,
defaultValuePlaceholderConstant,
} from '../../../../../registry/complete_items';
import { parametersFromHintsMap } from '../../parameters_from_hints';

type FunctionParamContext = NonNullable<ExpressionContext['options']['functionParameterContext']>;

Expand Down Expand Up @@ -70,7 +72,7 @@ async function handleFunctionParameterContext(
}

// Try exclusive suggestions first (COUNT(*), enum values)
const exclusiveSuggestions = tryExclusiveSuggestions(functionParamContext, ctx);
const exclusiveSuggestions = await tryExclusiveSuggestions(functionParamContext, ctx);

if (exclusiveSuggestions.length > 0) {
return exclusiveSuggestions;
Expand All @@ -81,10 +83,10 @@ async function handleFunctionParameterContext(
}

/** Try suggestions that are exclusive (if present, return only these) */
function tryExclusiveSuggestions(
async function tryExclusiveSuggestions(
functionParamContext: FunctionParamContext,
ctx: ExpressionContext
): ISuggestionItem[] {
): Promise<ISuggestionItem[]> {
const { functionDefinition, paramDefinitions } = functionParamContext;
const { options } = ctx;

Expand All @@ -95,11 +97,16 @@ function tryExclusiveSuggestions(
Boolean(functionParamContext.hasMoreMandatoryArgs),
options.isCursorFollowedByComma ?? false
);

if (enumItems.length > 0) {
return enumItems;
}

// Some parameters suggests special values that are deduced from the hints object provided by ES.
const itemsFromHints = await buildSuggestionsFromHints(paramDefinitions, ctx);
if (itemsFromHints.length > 0) {
return itemsFromHints;
}

return [];
}

Expand Down Expand Up @@ -374,6 +381,23 @@ function buildEnumValueSuggestions(
});
}

async function buildSuggestionsFromHints(
paramDefinitions: FunctionParameter[],
ctx: ExpressionContext
): Promise<ISuggestionItem[]> {
// Keep the hints that are unique by entityType + constraints
const hints: ParameterHint[] = uniqWith(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need the uniq?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's to make it more robust, signatures can be redundant with parameters as ES creates permutations with the parameters, so I don't want to create duplicated suggestions if the 'inference_id' param appears in multiple signatures.
At the same time, if the 'inference_id' appears in multiple signatures but with different constraints (different task_types) I DO want to fetch the different endpoints.

We don't have cases now, but we probably will not be notified if they change this signature to these cases, so better be prepared as it's just a line code.

... signature 1...
params: [
        {
          name: 'inference_id',
          type: 'keyword',
          hint: {
            entityType: 'inference_endpoint',
            constraints: {
              task_type: 'text_embedding',
            },
          },
        },
      ],
... signature 2...
params: [
        {
          name: 'inference_id',
          type: 'keyword',
          hint: {
            entityType: 'inference_endpoint',
            constraints: {
              task_type: 'rerank',
            },
          },
        },
      ],

paramDefinitions.flatMap(({ hint }) => hint ?? []),
(a, b) => a.entityType === b.entityType && isEqual(a.constraints, b.constraints)
);

const results = await Promise.all(
hints.map((hint) => parametersFromHintsMap[hint.entityType]?.(hint, ctx) ?? [])
);

return results.flat();
}

/** Builds suggestions for constant-only literal parameters */
function buildConstantOnlyLiteralSuggestions(
paramDefinitions: FunctionParameter[],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/

import type { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types';
import { i18n } from '@kbn/i18n';
import type { ParameterHint, ParameterHintEntityType } from '../../..';
import type { ISuggestionItem } from '../../../registry/types';
import type { ExpressionContext } from './expressions/types';
import { createInferenceEndpointToCompletionItem } from './helpers';

/**
* For some parameters, ES gives as hints about the nature of it, that we use to provide
* custom autocompletion handlers.
*/
export const parametersFromHintsMap: Record<
ParameterHintEntityType,
(hint: ParameterHint, ctx: ExpressionContext) => Promise<ISuggestionItem[]>
> = {
['inference_endpoint']: inferenceEndpointHandler,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need better types here otherwise everytime they add a hint we need to change this part.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh it was on purpose, it's a way of getting notified if they add a new hint, so we know we might want to add support to it, fixing the type error it's just adding ['new_hint']: () => [],, and we can create an issue to tackle it later.

I can add a Partial<> to it, if you prefer to avoid this.

export const parametersFromHintsMap: Partial<
  Record<
    ParameterHintEntityType,
    (hint: ParameterHint, ctx: ExpressionContext) => Promise<ISuggestionItem[]>
  >
>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes it makes it too hardcoded. We need an architecture as we have for functions. So something that we dont need to bother every time they add something. So have that in mind

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you mean with hardcoded?
Each new hint type will need a dedicated handler, as each hint will be very different from each other right? 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

};

async function inferenceEndpointHandler(
hint: ParameterHint,
ctx: ExpressionContext
): Promise<ISuggestionItem[]> {
if (hint.constraints?.task_type) {
const inferenceEnpoints =
(
await ctx.callbacks?.getInferenceEndpoints?.(
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should follow the other context logic and not having the callback here

hint.constraints?.task_type as InferenceTaskType
)
)?.inferenceEndpoints || [];

return inferenceEnpoints.map(createInferenceEndpointToCompletionItem).map((item) => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need the 2 maps?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed in new branch 67a221e

return {
...item,
detail: i18n.translate('kbn-esql-ast.esql.definitions.inferenceEndpointInFunction', {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is it visible (I didn't test, can you provide a screenshot?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the documentation detail of the suggestion
image

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok so this text is wrong but we can discuss after #246495 (comment)

defaultMessage: 'Inference endpoint used for this function',
}),
text: `"${item.text}"`,
};
});
}
return [];
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,11 @@ import type {
ESQLControlVariable,
ESQLSourceResult,
ESQLFieldWithMetadata,
InferenceEndpointsAutocompleteResult,
} from '@kbn/esql-types';
import type { LicenseType } from '@kbn/licensing-types';
import type { PricingProduct } from '@kbn/core-pricing-common/src/types';
import type { InferenceTaskType } from '@elastic/elasticsearch/lib/api/types';
import type { ESQLLocation } from '../../types';
import type { SupportedDataType } from '../definitions/types';
import type { EditorExtensions } from './options/recommended_queries';
Expand Down Expand Up @@ -138,6 +140,9 @@ export interface ICommandCallbacks {
getJoinIndices?: () => Promise<{ indices: IndexAutocompleteItem[] }>;
canCreateLookupIndex?: (indexName: string) => Promise<boolean>;
isServerless?: boolean;
getInferenceEndpoints?: (
taskType: InferenceTaskType
) => Promise<InferenceEndpointsAutocompleteResult>;
}

export interface ICommandContext {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2010,4 +2010,14 @@ describe('functions arg suggestions', () => {
expect(labels).not.toContain(',');
});
});

describe('function parameter built from hint', () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of hardcoding them every time they add a hint lets create these tests automatically from the info ES sent to us

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, I don't see it 🤔 💭, hints could vary a lot one from each other, some may need to mock new endpoints, etc. They tell us, hey this param is special, here it's a hint of how to create the suggestions for it, but each hint needs a different implementation from each other, so I'm not seeing how to make a generic test for all u.u

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You have the function from the hints, so I am hoping that we can create generic tests that we show suggestions for the hints without every time to have to hard code them (the less work we do for this the better, it means that our architecture can scale). In my mind it should be doable at least 😄

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I get your idea now.
But it would leave the actual handler untested. So we will still need to create a test for the dedicated hint handler to have good coverage.

I liked this approach more because it test both the handler and the autocomplete routine integration all together, with less code.
I can go with your solution too if you want and create specific tests for the handlers, in the bright side of it, they will be closer. It's all a matter of trade offs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I prefer a generic solution and then yes specific tests for the handlers would be ok 👌

it('suggests inference endpoints for TEXT_EMBEDDING function', async () => {
const { suggest } = await setup();
const suggestions = await suggest('FROM index | EVAL result = TEXT_EMBEDDING("text", /');
const labels = suggestions.map(({ label }) => label);

expect(labels).toEqual(['inference_1']);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,7 @@ async function getSuggestionsWithinCommandExpression(
hasMinimumLicenseRequired,
canCreateLookupIndex: callbacks?.canCreateLookupIndex,
isServerless: callbacks?.isServerless,
getInferenceEndpoints: callbacks?.getInferenceEndpoints,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer the inferences to come on the context

export interface ICommandContext {
  columns: Map<string, ESQLColumnData>;
  sources?: ESQLSourceResult[];
  joinSources?: IndexAutocompleteItem[];
  timeSeriesSources?: IndexAutocompleteItem[];
  inferenceEndpoints?: InferenceEndpointAutocompleteItem[];
  policies?: Map<string, ESQLPolicy>;
  editorExtensions?: EditorExtensions;
  variables?: ESQLControlVariable[];
  supportsControls?: boolean;
  histogramBarTarget?: number;
  activeProduct?: PricingProduct | undefined;
  isCursorInSubquery?: boolean;
}

We are having them already at the context, it is confusing to have them at the callbacks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My first idea was using the Command context yes, but did found some drawbacks:

These are suggestions for functions, not commands, in the command context we check if the command is COMPLETION or RERANK and fetch that data.

So the implementation is less straightforward: when creating each command context we would need to walk the command and check the signature of every function within the command to check if some argument has a hint, if it has one do the fetching of the data in case we need it later. This is in theory good for user perceived performance as we have the data before we need it, but adds extra parsing code early on in the code and fetches for data that may not be used, and IMO the difference in speed it's not really noticeable.

In contrast the current implementation makes use of the signature detection that is already been used for other suggestions, so at that point of the flow it's easy to detect if we have hints in it and if we need to fetch for data, and when we do it's because it's needed.

So, putting that on the table, there are some tradeoffs between solutions, I think the current one is simpler, adds less code to maintain and blends with the flow we use for suggesting within functions.
If we go for adding it into the context, it could have better perceived performance and unifies the "resources" available for the command in that object, but that comes with having to do deep analysis early on in the autocomplete routine for each command.

Summarising I think it's a decision about:
1 - Gathering all data we may need for autocomplete early on in the routine.
2 - Or fetching the data as we need it in the subroutines.

Let me know your thoughts with this new information, and if the directrice is to always go with option 1, which is totally fine to me btw :)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is problematic to have this call in 2 different places. It shows that something goes wrong with the architecture. As long as we have this information on the context, adding this also to the callbacks is wrong.

We should stick with our current one I think (otherwise we need to re-architect this thing). So I think we should go with the more complex one as it aligns with our current architeture.

Can you create another PR with this to see how this will be? If it doesnt look ok then we should think more thoroughly and re-architect!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do 👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stratoula what do you think about this implementation path -> #246736
(it still need some love before being prod ready, but want to validate we want to go that path before polishing the deatils.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes I really like it!! 👏

},
context,
offset
Expand Down