-
Notifications
You must be signed in to change notification settings - Fork 25.8k
[ES|QL] TEXT_EMBEDDING function definition #135059
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
987a709
516a0b6
36df7cf
918bdb7
4418a32
4bb147d
ddf3db5
aadf880
6fb48b0
2c423fb
9406d37
774986f
39b4323
8d4a832
71bae69
1275582
89dfcec
39919fa
b7e821d
97ecf80
d5cf81c
778d5e7
0d01b5e
e8ca515
5b56232
128688b
a8b99c4
a0cc965
ca85b86
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| placeholder | ||
| required_capability: text_embedding_function | ||
| required_capability: not_existing_capability | ||
|
|
||
| // tag::embedding-eval[] | ||
| ROW input="Who is Victor Hugo?" | ||
| | EVAL embedding = TEXT_EMBEDDING("Who is Victor Hugo?", "test_dense_inference") | ||
| ; | ||
| // end::embedding-eval[] | ||
|
|
||
|
|
||
| input:keyword | embedding:dense_vector | ||
| Who is Victor Hugo? | [56.0, 50.0, 48.0] | ||
| ; | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| /* | ||
| * 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; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| package org.elasticsearch.xpack.esql.expression.function.inference; | ||
|
|
||
| import org.elasticsearch.inference.TaskType; | ||
| import org.elasticsearch.xpack.esql.core.expression.Expression; | ||
| import org.elasticsearch.xpack.esql.core.expression.function.Function; | ||
| import org.elasticsearch.xpack.esql.core.tree.Source; | ||
|
|
||
| import java.util.List; | ||
|
|
||
| /** | ||
| * Base class for ESQL functions that use inference endpoints (e.g., TEXT_EMBEDDING). | ||
| */ | ||
| public abstract class InferenceFunction<PlanType extends InferenceFunction<PlanType>> extends Function { | ||
|
|
||
| public static final String INFERENCE_ID_PARAMETER_NAME = "inference_id"; | ||
|
|
||
| protected InferenceFunction(Source source, List<Expression> children) { | ||
| super(source, children); | ||
| } | ||
|
|
||
| /** The inference endpoint identifier expression. */ | ||
| public abstract Expression inferenceId(); | ||
|
|
||
| /** The task type required by this function (e.g., TEXT_EMBEDDING). */ | ||
| public abstract TaskType taskType(); | ||
|
|
||
| /** Returns a copy with inference resolution error for display to user. */ | ||
| public abstract PlanType withInferenceResolutionError(String inferenceId, String error); | ||
|
|
||
| /** True if this function contains nested inference function calls. */ | ||
| public boolean hasNestedInferenceFunction() { | ||
| return anyMatch(e -> e instanceof InferenceFunction && e != this); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,157 @@ | ||
| /* | ||
| * 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; you may not use this file except in compliance with the Elastic License | ||
| * 2.0. | ||
| */ | ||
|
|
||
| package org.elasticsearch.xpack.esql.expression.function.inference; | ||
|
|
||
| import org.elasticsearch.common.io.stream.StreamOutput; | ||
| import org.elasticsearch.inference.TaskType; | ||
| import org.elasticsearch.xpack.esql.core.expression.Expression; | ||
| import org.elasticsearch.xpack.esql.core.expression.UnresolvedAttribute; | ||
| import org.elasticsearch.xpack.esql.core.tree.NodeInfo; | ||
| import org.elasticsearch.xpack.esql.core.tree.Source; | ||
| import org.elasticsearch.xpack.esql.core.type.DataType; | ||
| import org.elasticsearch.xpack.esql.expression.function.Example; | ||
| import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesTo; | ||
| import org.elasticsearch.xpack.esql.expression.function.FunctionAppliesToLifecycle; | ||
| import org.elasticsearch.xpack.esql.expression.function.FunctionInfo; | ||
| import org.elasticsearch.xpack.esql.expression.function.Param; | ||
|
|
||
| import java.io.IOException; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
|
|
||
| import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.FIRST; | ||
| import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.ParamOrdinal.SECOND; | ||
| import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isFoldable; | ||
| import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isNotNull; | ||
| import static org.elasticsearch.xpack.esql.core.expression.TypeResolutions.isType; | ||
|
|
||
| /** | ||
| * TEXT_EMBEDDING function converts text to dense vector embeddings using an inference endpoint. | ||
| */ | ||
| public class TextEmbedding extends InferenceFunction<TextEmbedding> { | ||
|
|
||
| private final Expression inferenceId; | ||
| private final Expression inputText; | ||
|
|
||
| @FunctionInfo( | ||
| returnType = "dense_vector", | ||
| description = "Generates dense vector embeddings for text using a specified inference endpoint.", | ||
| appliesTo = { @FunctionAppliesTo(lifeCycle = FunctionAppliesToLifecycle.DEVELOPMENT) }, | ||
| preview = true, | ||
| examples = { | ||
| @Example( | ||
| description = "Generate text embeddings using the 'test_dense_inference' inference endpoint.", | ||
| file = "text-embedding", | ||
| tag = "embedding-eval" | ||
| ) } | ||
| ) | ||
| public TextEmbedding( | ||
| Source source, | ||
| @Param(name = "text", type = { "keyword" }, description = "Text to generate embeddings from") Expression inputText, | ||
| @Param( | ||
afoucret marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| name = InferenceFunction.INFERENCE_ID_PARAMETER_NAME, | ||
| type = { "keyword" }, | ||
| description = "Identifier of the inference endpoint" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we need to add a function example and then regen the docs
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this should be fixed in Kibana CI so it does not break in such a case. I mean, it is expected for a function that have Anyway, I added an placeholder example so I will not break anything, It will be replaced when adding more realistic CSV tests. |
||
| ) Expression inferenceId | ||
| ) { | ||
| super(source, List.of(inputText, inferenceId)); | ||
| this.inferenceId = inferenceId; | ||
| this.inputText = inputText; | ||
| } | ||
|
|
||
| @Override | ||
| public void writeTo(StreamOutput out) throws IOException { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we need to serialize this function - since we will always resolve it on the coordinator and replace it with its result.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would prefer to keep the serialization even if the expression is not supposed to be moved between node. The first reason is because the Also I consider that the implementation or serialization / deserialization is simple to implement and it is probably not beneficial to let it unimplemented.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
That does not mean it should. If the text_embedding function is actually serialized and sent between nodes, that's an execution path that should never happen. Anything that extends from I get that making this unserializable is more work, especially for
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It feels to me that we are removing some code that we will ultimately reintroduce when we will support non constant input text embeddings. Anyway, I pushed a version without serialization so we can move forward and merge this PR. |
||
| throw new UnsupportedOperationException("doesn't escape the node"); | ||
| } | ||
|
|
||
| @Override | ||
| public String getWriteableName() { | ||
| throw new UnsupportedOperationException("doesn't escape the node"); | ||
| } | ||
|
|
||
| public Expression inputText() { | ||
| return inputText; | ||
| } | ||
|
|
||
| @Override | ||
| public Expression inferenceId() { | ||
| return inferenceId; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean foldable() { | ||
| return inferenceId.foldable() && inputText.foldable(); | ||
| } | ||
|
|
||
| @Override | ||
| public DataType dataType() { | ||
| return DataType.DENSE_VECTOR; | ||
| } | ||
|
|
||
| @Override | ||
| protected TypeResolution resolveType() { | ||
| if (childrenResolved() == false) { | ||
| return new TypeResolution("Unresolved children"); | ||
| } | ||
|
|
||
| TypeResolution textResolution = isNotNull(inputText, sourceText(), FIRST).and(isFoldable(inputText, sourceText(), FIRST)) | ||
| .and(isType(inputText, DataType.KEYWORD::equals, sourceText(), FIRST, "string")); | ||
|
|
||
| if (textResolution.unresolved()) { | ||
| return textResolution; | ||
| } | ||
|
|
||
| TypeResolution inferenceIdResolution = isNotNull(inferenceId, sourceText(), SECOND).and( | ||
| isType(inferenceId, DataType.KEYWORD::equals, sourceText(), SECOND, "string") | ||
| ).and(isFoldable(inferenceId, sourceText(), SECOND)); | ||
|
|
||
| if (inferenceIdResolution.unresolved()) { | ||
| return inferenceIdResolution; | ||
| } | ||
|
|
||
| return TypeResolution.TYPE_RESOLVED; | ||
| } | ||
|
|
||
| @Override | ||
| public TaskType taskType() { | ||
| return TaskType.TEXT_EMBEDDING; | ||
| } | ||
|
|
||
| @Override | ||
| public TextEmbedding withInferenceResolutionError(String inferenceId, String error) { | ||
| return new TextEmbedding(source(), inputText, new UnresolvedAttribute(inferenceId().source(), inferenceId, error)); | ||
| } | ||
|
|
||
| @Override | ||
| public Expression replaceChildren(List<Expression> newChildren) { | ||
| return new TextEmbedding(source(), newChildren.get(0), newChildren.get(1)); | ||
| } | ||
|
|
||
| @Override | ||
| protected NodeInfo<? extends Expression> info() { | ||
| return NodeInfo.create(this, TextEmbedding::new, inputText, inferenceId); | ||
| } | ||
|
|
||
| @Override | ||
| public String toString() { | ||
| return "TEXT_EMBEDDING(" + inputText + ", " + inferenceId + ")"; | ||
| } | ||
|
|
||
| @Override | ||
| public boolean equals(Object o) { | ||
| if (o == null || getClass() != o.getClass()) return false; | ||
| if (super.equals(o) == false) return false; | ||
| TextEmbedding textEmbedding = (TextEmbedding) o; | ||
| return Objects.equals(inferenceId, textEmbedding.inferenceId) && Objects.equals(inputText, textEmbedding.inputText); | ||
| } | ||
|
|
||
| @Override | ||
| public int hashCode() { | ||
| return Objects.hash(super.hashCode(), inferenceId, inputText); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Non-blocking question for my ES|QL education: Can you help me understand why this change?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will transform all the children in the plan that have the class
InferencePlanwith the result of theresolveInferencePlan(p, context)call. Inference plans are typically command using inference:RERANKandCOMPLETIONwill transform do the same but for
InferenceFunctioninstead of plan. Because text embedding is our first inference function this was not yet done, so I added it.