From f375daa4ebca4bcfbee361422951218d30383702 Mon Sep 17 00:00:00 2001 From: Octavio Cuenca Date: Sat, 21 Feb 2026 15:12:00 -0500 Subject: [PATCH 1/2] Add agentic query support to the Java client Adds AgenticQuery as a new variant of the Query tagged union, enabling natural language questions to be translated into DSL queries via a preconfigured agent and search pipeline (OpenSearch 3.2+). Resolves https://github.com/opensearch-project/opensearch-java/issues/1892 Signed-off-by: Octavio Cuenca Co-Authored-By: Claude Opus 4.6 (1M context) --- CHANGELOG.md | 1 + .../_types/query_dsl/AgenticQuery.java | 304 ++++++++++++++++++ .../opensearch/_types/query_dsl/Query.java | 30 +- .../_types/query_dsl/QueryBuilders.java | 7 + .../_types/query_dsl/AgenticQueryTest.java | 26 ++ 5 files changed, 367 insertions(+), 1 deletion(-) create mode 100644 java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQuery.java create mode 100644 java-client/src/test/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQueryTest.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 6adb3b3ecb..fd256b7acd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) - Bump `com.carrotsearch.randomizedtesting:randomizedtesting-runner` from 2.8.3 to 2.8.4 ([#1882](https://github.com/opensearch-project/opensearch-java/pull/1882)) ### Added +- Added support for `agentic` query type to translate natural language questions into DSL queries ### Fixed - Fix formatting of the main method to run for various samples ([#1749](https://github.com/opensearch-project/opensearch-java/pull/1749)) diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQuery.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQuery.java new file mode 100644 index 0000000000..bb12d1beff --- /dev/null +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQuery.java @@ -0,0 +1,304 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +/* + * Modifications Copyright OpenSearch Contributors. See + * GitHub history for details. + */ + +//---------------------------------------------------- +// THIS CODE IS GENERATED. MANUAL EDITS WILL BE LOST. +//---------------------------------------------------- + +package org.opensearch.client.opensearch._types.query_dsl; + +import jakarta.json.stream.JsonGenerator; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import javax.annotation.Generated; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import org.opensearch.client.json.JsonpDeserializable; +import org.opensearch.client.json.JsonpDeserializer; +import org.opensearch.client.json.JsonpMapper; +import org.opensearch.client.json.ObjectBuilderDeserializer; +import org.opensearch.client.json.ObjectDeserializer; +import org.opensearch.client.util.ApiTypeHelper; +import org.opensearch.client.util.CopyableBuilder; +import org.opensearch.client.util.ObjectBuilder; +import org.opensearch.client.util.ToCopyableBuilder; + +// typedef: _types.query_dsl.AgenticQuery + +@JsonpDeserializable +@Generated("org.opensearch.client.codegen.CodeGenerator") +public class AgenticQuery extends QueryBase implements QueryVariant, ToCopyableBuilder { + + @Nonnull + private final String queryText; + + @Nonnull + private final List queryFields; + + @Nullable + private final String memoryId; + + // --------------------------------------------------------------------------------------------- + + private AgenticQuery(Builder builder) { + super(builder); + this.queryText = ApiTypeHelper.requireNonNull(builder.queryText, this, "queryText"); + this.queryFields = ApiTypeHelper.unmodifiable(builder.queryFields); + this.memoryId = builder.memoryId; + } + + public static AgenticQuery of(Function> fn) { + return fn.apply(new Builder()).build(); + } + + /** + * {@link Query} variant kind. + */ + @Override + public Query.Kind _queryKind() { + return Query.Kind.Agentic; + } + + /** + * Required - The natural language question. + *

+ * API name: {@code query_text} + *

+ */ + @Nonnull + public final String queryText() { + return this.queryText; + } + + /** + * Index fields the agent should consider. + *

+ * API name: {@code query_fields} + *

+ */ + @Nonnull + public final List queryFields() { + return this.queryFields; + } + + /** + * Memory ID for conversational context. + *

+ * API name: {@code memory_id} + *

+ */ + @Nullable + public final String memoryId() { + return this.memoryId; + } + + protected void serializeInternal(JsonGenerator generator, JsonpMapper mapper) { + super.serializeInternal(generator, mapper); + generator.writeKey("query_text"); + generator.write(this.queryText); + + if (ApiTypeHelper.isDefined(this.queryFields)) { + generator.writeKey("query_fields"); + generator.writeStartArray(); + for (String item0 : this.queryFields) { + generator.write(item0); + } + generator.writeEnd(); + } + + if (this.memoryId != null) { + generator.writeKey("memory_id"); + generator.write(this.memoryId); + } + } + + // --------------------------------------------------------------------------------------------- + + @Override + @Nonnull + public Builder toBuilder() { + return new Builder(this); + } + + @Nonnull + public static Builder builder() { + return new Builder(); + } + + /** + * Builder for {@link AgenticQuery}. + */ + public static class Builder extends QueryBase.AbstractBuilder implements CopyableBuilder { + private String queryText; + @Nullable + private List queryFields; + @Nullable + private String memoryId; + + public Builder() {} + + private Builder(AgenticQuery o) { + super(o); + this.queryText = o.queryText; + this.queryFields = _listCopy(o.queryFields); + this.memoryId = o.memoryId; + } + + private Builder(Builder o) { + super(o); + this.queryText = o.queryText; + this.queryFields = _listCopy(o.queryFields); + this.memoryId = o.memoryId; + } + + @Override + @Nonnull + public Builder copy() { + return new Builder(this); + } + + @Override + @Nonnull + protected Builder self() { + return this; + } + + /** + * Required - The natural language question. + *

+ * API name: {@code query_text} + *

+ */ + @Nonnull + public final Builder queryText(String value) { + this.queryText = value; + return this; + } + + /** + * Index fields the agent should consider. + *

+ * API name: {@code query_fields} + *

+ * + *

+ * Adds all elements of list to queryFields. + *

+ */ + @Nonnull + public final Builder queryFields(List list) { + this.queryFields = _listAddAll(this.queryFields, list); + return this; + } + + /** + * Index fields the agent should consider. + *

+ * API name: {@code query_fields} + *

+ * + *

+ * Adds one or more values to queryFields. + *

+ */ + @Nonnull + public final Builder queryFields(String value, String... values) { + this.queryFields = _listAdd(this.queryFields, value, values); + return this; + } + + /** + * Memory ID for conversational context. + *

+ * API name: {@code memory_id} + *

+ */ + @Nonnull + public final Builder memoryId(@Nullable String value) { + this.memoryId = value; + return this; + } + + /** + * Builds a {@link AgenticQuery}. + * + * @throws NullPointerException if some of the required fields are null. + */ + @Override + @Nonnull + public AgenticQuery build() { + _checkSingleUse(); + + return new AgenticQuery(this); + } + } + + // --------------------------------------------------------------------------------------------- + + /** + * Json deserializer for {@link AgenticQuery} + */ + public static final JsonpDeserializer _DESERIALIZER = ObjectBuilderDeserializer.lazy( + Builder::new, + AgenticQuery::setupAgenticQueryDeserializer + ); + + protected static void setupAgenticQueryDeserializer(ObjectDeserializer op) { + setupQueryBaseDeserializer(op); + op.add(Builder::queryText, JsonpDeserializer.stringDeserializer(), "query_text"); + op.add(Builder::queryFields, JsonpDeserializer.arrayDeserializer(JsonpDeserializer.stringDeserializer()), "query_fields"); + op.add(Builder::memoryId, JsonpDeserializer.stringDeserializer(), "memory_id"); + } + + @Override + public int hashCode() { + int result = super.hashCode(); + result = 31 * result + this.queryText.hashCode(); + result = 31 * result + Objects.hashCode(this.queryFields); + result = 31 * result + Objects.hashCode(this.memoryId); + return result; + } + + @Override + public boolean equals(Object o) { + if (!super.equals(o)) { + return false; + } + if (this == o) return true; + if (o == null || this.getClass() != o.getClass()) return false; + AgenticQuery other = (AgenticQuery) o; + return this.queryText.equals(other.queryText) + && Objects.equals(this.queryFields, other.queryFields) + && Objects.equals(this.memoryId, other.memoryId); + } +} diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java index 6186db7e45..b73f063ee8 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java @@ -1,4 +1,4 @@ -/* + /* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to @@ -68,6 +68,7 @@ public class Query implements TaggedUnion, AggregationVarian * {@link Query} variant kinds. */ public enum Kind implements JsonEnum { + Agentic("agentic"), Bool("bool"), Boosting("boosting"), CombinedFields("combined_fields"), @@ -173,6 +174,22 @@ public static Query of(Function> fn) { return fn.apply(new Builder()).build(); } + /** + * Is this variant instance of kind {@code agentic}? + */ + public boolean isAgentic() { + return _kind == Kind.Agentic; + } + + /** + * Get the {@code agentic} variant value. + * + * @throws IllegalStateException if the current variant is not the {@code agentic} kind. + */ + public AgenticQuery agentic() { + return TaggedUnionUtils.get(this, Kind.Agentic); + } + /** * Is this variant instance of kind {@code bool}? */ @@ -1127,6 +1144,16 @@ private Builder(Query o) { this._value = o._value; } + public ObjectBuilder agentic(AgenticQuery v) { + this._kind = Kind.Agentic; + this._value = v; + return this; + } + + public ObjectBuilder agentic(Function> fn) { + return this.agentic(fn.apply(new AgenticQuery.Builder()).build()); + } + public ObjectBuilder bool(BoolQuery v) { this._kind = Kind.Bool; this._value = v; @@ -1701,6 +1728,7 @@ public Query build() { } protected static void setupQueryDeserializer(ObjectDeserializer op) { + op.add(Builder::agentic, AgenticQuery._DESERIALIZER, "agentic"); op.add(Builder::bool, BoolQuery._DESERIALIZER, "bool"); op.add(Builder::boosting, BoostingQuery._DESERIALIZER, "boosting"); op.add(Builder::combinedFields, CombinedFieldsQuery._DESERIALIZER, "combined_fields"); diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/QueryBuilders.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/QueryBuilders.java index ae3c17b27d..1fb6cde855 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/QueryBuilders.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/QueryBuilders.java @@ -50,6 +50,13 @@ public class QueryBuilders { private QueryBuilders() {} + /** + * Creates a builder for the {@link AgenticQuery agentic} {@code Query} variant. + */ + public static AgenticQuery.Builder agentic() { + return new AgenticQuery.Builder(); + } + /** * Creates a builder for the {@link BoolQuery bool} {@code Query} variant. */ diff --git a/java-client/src/test/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQueryTest.java b/java-client/src/test/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQueryTest.java new file mode 100644 index 0000000000..88df53b1b7 --- /dev/null +++ b/java-client/src/test/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQueryTest.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * + * The OpenSearch Contributors require contributions made to + * this file be licensed under the Apache-2.0 license or a + * compatible open source license. + */ + +package org.opensearch.client.opensearch._types.query_dsl; + +import java.util.Arrays; +import org.junit.Test; +import org.opensearch.client.opensearch.model.ModelTestCase; + +public class AgenticQueryTest extends ModelTestCase { + @Test + public void toBuilder() { + AgenticQuery origin = new AgenticQuery.Builder().queryText("What is the best selling product?") + .queryFields(Arrays.asList("title", "description")) + .memoryId("conv-123") + .build(); + AgenticQuery copied = origin.toBuilder().build(); + + assertEquals(toJson(copied), toJson(origin)); + } +} From 5a7e96700609888e22a0e63db670bcdf55c8d0e1 Mon Sep 17 00:00:00 2001 From: Octavio Cuenca Date: Sat, 21 Feb 2026 16:56:57 -0500 Subject: [PATCH 2/2] Add AgenticQuery schema to OpenAPI spec and regenerate code Adds the `_common.query_dsl___AgenticQuery` schema to the local opensearch-openapi.yaml spec and registers the `agentic` variant in `_common.query_dsl___QueryContainer`, so the generated Java classes (AgenticQuery, Query, QueryBuilders) are properly derived from the spec rather than written by hand. Co-Authored-By: Claude Sonnet 4.6 (1M context) --- .../_types/query_dsl/AgenticQuery.java | 86 +++++++++---------- .../opensearch/_types/query_dsl/Query.java | 2 +- java-codegen/opensearch-openapi.yaml | 20 +++++ 3 files changed, 64 insertions(+), 44 deletions(-) diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQuery.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQuery.java index bb12d1beff..8e517c41aa 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQuery.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/AgenticQuery.java @@ -59,22 +59,22 @@ @Generated("org.opensearch.client.codegen.CodeGenerator") public class AgenticQuery extends QueryBase implements QueryVariant, ToCopyableBuilder { - @Nonnull - private final String queryText; + @Nullable + private final String memoryId; @Nonnull private final List queryFields; - @Nullable - private final String memoryId; + @Nonnull + private final String queryText; // --------------------------------------------------------------------------------------------- private AgenticQuery(Builder builder) { super(builder); - this.queryText = ApiTypeHelper.requireNonNull(builder.queryText, this, "queryText"); - this.queryFields = ApiTypeHelper.unmodifiable(builder.queryFields); this.memoryId = builder.memoryId; + this.queryFields = ApiTypeHelper.unmodifiable(builder.queryFields); + this.queryText = ApiTypeHelper.requireNonNull(builder.queryText, this, "queryText"); } public static AgenticQuery of(Function> fn) { @@ -90,14 +90,14 @@ public Query.Kind _queryKind() { } /** - * Required - The natural language question. + * Memory ID for conversational context. *

- * API name: {@code query_text} + * API name: {@code memory_id} *

*/ - @Nonnull - public final String queryText() { - return this.queryText; + @Nullable + public final String memoryId() { + return this.memoryId; } /** @@ -112,20 +112,22 @@ public final List queryFields() { } /** - * Memory ID for conversational context. + * Required - The natural language question. *

- * API name: {@code memory_id} + * API name: {@code query_text} *

*/ - @Nullable - public final String memoryId() { - return this.memoryId; + @Nonnull + public final String queryText() { + return this.queryText; } protected void serializeInternal(JsonGenerator generator, JsonpMapper mapper) { super.serializeInternal(generator, mapper); - generator.writeKey("query_text"); - generator.write(this.queryText); + if (this.memoryId != null) { + generator.writeKey("memory_id"); + generator.write(this.memoryId); + } if (ApiTypeHelper.isDefined(this.queryFields)) { generator.writeKey("query_fields"); @@ -136,10 +138,8 @@ protected void serializeInternal(JsonGenerator generator, JsonpMapper mapper) { generator.writeEnd(); } - if (this.memoryId != null) { - generator.writeKey("memory_id"); - generator.write(this.memoryId); - } + generator.writeKey("query_text"); + generator.write(this.queryText); } // --------------------------------------------------------------------------------------------- @@ -159,26 +159,26 @@ public static Builder builder() { * Builder for {@link AgenticQuery}. */ public static class Builder extends QueryBase.AbstractBuilder implements CopyableBuilder { - private String queryText; - @Nullable - private List queryFields; @Nullable private String memoryId; + @Nullable + private List queryFields; + private String queryText; public Builder() {} private Builder(AgenticQuery o) { super(o); - this.queryText = o.queryText; - this.queryFields = _listCopy(o.queryFields); this.memoryId = o.memoryId; + this.queryFields = _listCopy(o.queryFields); + this.queryText = o.queryText; } private Builder(Builder o) { super(o); - this.queryText = o.queryText; - this.queryFields = _listCopy(o.queryFields); this.memoryId = o.memoryId; + this.queryFields = _listCopy(o.queryFields); + this.queryText = o.queryText; } @Override @@ -194,14 +194,14 @@ protected Builder self() { } /** - * Required - The natural language question. + * Memory ID for conversational context. *

- * API name: {@code query_text} + * API name: {@code memory_id} *

*/ @Nonnull - public final Builder queryText(String value) { - this.queryText = value; + public final Builder memoryId(@Nullable String value) { + this.memoryId = value; return this; } @@ -238,14 +238,14 @@ public final Builder queryFields(String value, String... values) { } /** - * Memory ID for conversational context. + * Required - The natural language question. *

- * API name: {@code memory_id} + * API name: {@code query_text} *

*/ @Nonnull - public final Builder memoryId(@Nullable String value) { - this.memoryId = value; + public final Builder queryText(String value) { + this.queryText = value; return this; } @@ -275,17 +275,17 @@ public AgenticQuery build() { protected static void setupAgenticQueryDeserializer(ObjectDeserializer op) { setupQueryBaseDeserializer(op); - op.add(Builder::queryText, JsonpDeserializer.stringDeserializer(), "query_text"); - op.add(Builder::queryFields, JsonpDeserializer.arrayDeserializer(JsonpDeserializer.stringDeserializer()), "query_fields"); op.add(Builder::memoryId, JsonpDeserializer.stringDeserializer(), "memory_id"); + op.add(Builder::queryFields, JsonpDeserializer.arrayDeserializer(JsonpDeserializer.stringDeserializer()), "query_fields"); + op.add(Builder::queryText, JsonpDeserializer.stringDeserializer(), "query_text"); } @Override public int hashCode() { int result = super.hashCode(); - result = 31 * result + this.queryText.hashCode(); - result = 31 * result + Objects.hashCode(this.queryFields); result = 31 * result + Objects.hashCode(this.memoryId); + result = 31 * result + Objects.hashCode(this.queryFields); + result = 31 * result + this.queryText.hashCode(); return result; } @@ -297,8 +297,8 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; AgenticQuery other = (AgenticQuery) o; - return this.queryText.equals(other.queryText) + return Objects.equals(this.memoryId, other.memoryId) && Objects.equals(this.queryFields, other.queryFields) - && Objects.equals(this.memoryId, other.memoryId); + && this.queryText.equals(other.queryText); } } diff --git a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java index b73f063ee8..c3255666c1 100644 --- a/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java +++ b/java-client/src/generated/java/org/opensearch/client/opensearch/_types/query_dsl/Query.java @@ -1,4 +1,4 @@ - /* +/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to diff --git a/java-codegen/opensearch-openapi.yaml b/java-codegen/opensearch-openapi.yaml index 0a241d0dbf..6a997742a7 100644 --- a/java-codegen/opensearch-openapi.yaml +++ b/java-codegen/opensearch-openapi.yaml @@ -47932,6 +47932,24 @@ components: - xy_shape required: - type + _common.query_dsl___AgenticQuery: + allOf: + - $ref: '#/components/schemas/_common.query_dsl___QueryBase' + - type: object + properties: + query_text: + description: The natural language question. + type: string + query_fields: + description: Index fields the agent should consider. + type: array + items: + type: string + memory_id: + description: Memory ID for conversational context. + type: string + required: + - query_text _common.query_dsl___BoolQuery: allOf: - $ref: '#/components/schemas/_common.query_dsl___QueryBase' @@ -49277,6 +49295,8 @@ components: _common.query_dsl___QueryContainer: type: object properties: + agentic: + $ref: '#/components/schemas/_common.query_dsl___AgenticQuery' bool: $ref: '#/components/schemas/_common.query_dsl___BoolQuery' boosting: