|
55 | 55 | import org.elasticsearch.xpack.esql.expression.function.fulltext.QueryString; |
56 | 56 | import org.elasticsearch.xpack.esql.expression.function.grouping.Bucket; |
57 | 57 | import org.elasticsearch.xpack.esql.expression.function.grouping.TBucket; |
| 58 | +import org.elasticsearch.xpack.esql.expression.function.inference.TextEmbedding; |
58 | 59 | import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToDateNanos; |
59 | 60 | import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToDatetime; |
60 | 61 | import org.elasticsearch.xpack.esql.expression.function.scalar.convert.ToInteger; |
|
121 | 122 | import static org.elasticsearch.xpack.esql.EsqlTestUtils.referenceAttribute; |
122 | 123 | import static org.elasticsearch.xpack.esql.EsqlTestUtils.withDefaultLimitWarning; |
123 | 124 | import static org.elasticsearch.xpack.esql.analysis.Analyzer.NO_FIELDS; |
| 125 | +import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.TEXT_EMBEDDING_INFERENCE_ID; |
124 | 126 | import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.analyze; |
125 | 127 | import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.analyzer; |
126 | 128 | import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.analyzerDefaultMapping; |
127 | 129 | import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.defaultEnrichResolution; |
128 | 130 | import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.defaultInferenceResolution; |
129 | 131 | import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.indexWithDateDateNanosUnionType; |
130 | 132 | import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.loadMapping; |
| 133 | +import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.randomInferenceId; |
131 | 134 | import static org.elasticsearch.xpack.esql.analysis.AnalyzerTestUtils.tsdbIndexResolution; |
132 | 135 | import static org.elasticsearch.xpack.esql.core.plugin.EsqlCorePlugin.DENSE_VECTOR_FEATURE_FLAG; |
133 | 136 | import static org.elasticsearch.xpack.esql.core.tree.Source.EMPTY; |
@@ -3629,6 +3632,115 @@ private void assertEmptyEsRelation(LogicalPlan plan) { |
3629 | 3632 | assertThat(esRelation.output(), equalTo(NO_FIELDS)); |
3630 | 3633 | } |
3631 | 3634 |
|
| 3635 | + public void testTextEmbeddingResolveInferenceId() { |
| 3636 | + assumeTrue("TEXT_EMBEDDING function required", EsqlCapabilities.Cap.TEXT_EMBEDDING_FUNCTION.isEnabled()); |
| 3637 | + |
| 3638 | + LogicalPlan plan = analyze( |
| 3639 | + """ |
| 3640 | + FROM books METADATA _score| EVAL embedding = TEXT_EMBEDDING("italian food recipe", "%s")""".formatted( |
| 3641 | + TEXT_EMBEDDING_INFERENCE_ID |
| 3642 | + ), |
| 3643 | + "mapping-books.json" |
| 3644 | + ); |
| 3645 | + |
| 3646 | + Eval eval = as(as(plan, Limit.class).child(), Eval.class); |
| 3647 | + assertThat(eval.fields(), hasSize(1)); |
| 3648 | + Alias alias = as(eval.fields().get(0), Alias.class); |
| 3649 | + assertThat(alias.name(), equalTo("embedding")); |
| 3650 | + TextEmbedding function = as(alias.child(), TextEmbedding.class); |
| 3651 | + |
| 3652 | + assertThat(function.inputText(), equalTo(string("italian food recipe"))); |
| 3653 | + assertThat(function.inferenceId(), equalTo(string(TEXT_EMBEDDING_INFERENCE_ID))); |
| 3654 | + } |
| 3655 | + |
| 3656 | + public void testTextEmbeddingFunctionResolveType() { |
| 3657 | + assumeTrue("TEXT_EMBEDDING function required", EsqlCapabilities.Cap.TEXT_EMBEDDING_FUNCTION.isEnabled()); |
| 3658 | + |
| 3659 | + LogicalPlan plan = analyze( |
| 3660 | + """ |
| 3661 | + FROM books METADATA _score| EVAL embedding = TEXT_EMBEDDING("italian food recipe", "%s")""".formatted( |
| 3662 | + TEXT_EMBEDDING_INFERENCE_ID |
| 3663 | + ), |
| 3664 | + "mapping-books.json" |
| 3665 | + ); |
| 3666 | + |
| 3667 | + Eval eval = as(as(plan, Limit.class).child(), Eval.class); |
| 3668 | + assertThat(eval.fields(), hasSize(1)); |
| 3669 | + Alias alias = as(eval.fields().get(0), Alias.class); |
| 3670 | + assertThat(alias.name(), equalTo("embedding")); |
| 3671 | + |
| 3672 | + TextEmbedding function = as(alias.child(), TextEmbedding.class); |
| 3673 | + |
| 3674 | + assertThat(function.foldable(), equalTo(true)); |
| 3675 | + assertThat(function.dataType(), equalTo(DENSE_VECTOR)); |
| 3676 | + } |
| 3677 | + |
| 3678 | + public void testTextEmbeddingFunctionMissingInferenceIdError() { |
| 3679 | + assumeTrue("TEXT_EMBEDDING function required", EsqlCapabilities.Cap.TEXT_EMBEDDING_FUNCTION.isEnabled()); |
| 3680 | + |
| 3681 | + VerificationException ve = expectThrows( |
| 3682 | + VerificationException.class, |
| 3683 | + () -> analyze( |
| 3684 | + """ |
| 3685 | + FROM books METADATA _score| EVAL embedding = TEXT_EMBEDDING("italian food recipe", "%s")""".formatted( |
| 3686 | + "unknow-inference-id" |
| 3687 | + ), |
| 3688 | + "mapping-books.json" |
| 3689 | + ) |
| 3690 | + ); |
| 3691 | + |
| 3692 | + assertThat(ve.getMessage(), containsString("unresolved inference [unknow-inference-id]")); |
| 3693 | + } |
| 3694 | + |
| 3695 | + public void testTextEmbeddingFunctionInvalidInferenceIdError() { |
| 3696 | + assumeTrue("TEXT_EMBEDDING function required", EsqlCapabilities.Cap.TEXT_EMBEDDING_FUNCTION.isEnabled()); |
| 3697 | + |
| 3698 | + String inferenceId = randomInferenceId(TEXT_EMBEDDING_INFERENCE_ID); |
| 3699 | + VerificationException ve = expectThrows( |
| 3700 | + VerificationException.class, |
| 3701 | + () -> analyze( |
| 3702 | + """ |
| 3703 | + FROM books METADATA _score| EVAL embedding = TEXT_EMBEDDING("italian food recipe", "%s")""".formatted(inferenceId), |
| 3704 | + "mapping-books.json" |
| 3705 | + ) |
| 3706 | + ); |
| 3707 | + |
| 3708 | + assertThat(ve.getMessage(), containsString("cannot use inference endpoint [%s] with task type".formatted(inferenceId))); |
| 3709 | + } |
| 3710 | + |
| 3711 | + public void testTextEmbeddingFunctionWithoutModel() { |
| 3712 | + assumeTrue("TEXT_EMBEDDING function required", EsqlCapabilities.Cap.TEXT_EMBEDDING_FUNCTION.isEnabled()); |
| 3713 | + |
| 3714 | + ParsingException ve = expectThrows(ParsingException.class, () -> analyze(""" |
| 3715 | + FROM books METADATA _score| EVAL embedding = TEXT_EMBEDDING("italian food recipe")""", "mapping-books.json")); |
| 3716 | + |
| 3717 | + assertThat( |
| 3718 | + ve.getMessage(), |
| 3719 | + containsString(" error building [text_embedding]: function [text_embedding] expects exactly two arguments") |
| 3720 | + ); |
| 3721 | + } |
| 3722 | + |
| 3723 | + public void testKnnFunctionWithTextEmbedding() { |
| 3724 | + assumeTrue("dense_vector capability not available", EsqlCapabilities.Cap.KNN_FUNCTION_V5.isEnabled()); |
| 3725 | + assumeTrue("TEXT_EMBEDDING function required", EsqlCapabilities.Cap.TEXT_EMBEDDING_FUNCTION.isEnabled()); |
| 3726 | + |
| 3727 | + String fieldName = randomFrom("float_vector", "byte_vector"); |
| 3728 | + |
| 3729 | + LogicalPlan plan = analyze(""" |
| 3730 | + from test | where KNN(%s, TEXT_EMBEDDING("italian food recipe", "%s")) |
| 3731 | + """.formatted(fieldName, TEXT_EMBEDDING_INFERENCE_ID), "mapping-dense_vector.json"); |
| 3732 | + |
| 3733 | + Limit limit = as(plan, Limit.class); |
| 3734 | + Filter filter = as(limit.child(), Filter.class); |
| 3735 | + Knn knn = as(filter.condition(), Knn.class); |
| 3736 | + assertThat(knn.field(), instanceOf(FieldAttribute.class)); |
| 3737 | + assertThat(((FieldAttribute) knn.field()).name(), equalTo(fieldName)); |
| 3738 | + |
| 3739 | + TextEmbedding textEmbedding = as(knn.query(), TextEmbedding.class); |
| 3740 | + assertThat(textEmbedding.inputText(), equalTo(string("italian food recipe"))); |
| 3741 | + assertThat(textEmbedding.inferenceId(), equalTo(string(TEXT_EMBEDDING_INFERENCE_ID))); |
| 3742 | + } |
| 3743 | + |
3632 | 3744 | public void testResolveRerankInferenceId() { |
3633 | 3745 | { |
3634 | 3746 | LogicalPlan plan = analyze(""" |
|
0 commit comments