Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
8a5f19b
feat: add transparent gRPC transport as separate java-client-grpc module
dayana-j Jul 15, 2026
51cd03e
ci: retrigger CI
dayana-j Jul 16, 2026
a7bbd43
Making the FieldMappingUtil package private
dayana-j Jul 20, 2026
874c174
Making this package private
dayana-j Jul 20, 2026
b7575b4
Making BasicAuthInterceptor package private
dayana-j Jul 20, 2026
a667a09
Remove unused toHttpStatus method and fix test package for package-pr…
dayana-j Jul 20, 2026
b8d8720
refactor: split AWS SigV4 into AwsGrpcTransport per maintainer feedback
dayana-j Jul 20, 2026
bd70c72
refactor: remove silent REST fallback from HybridTransport
dayana-j Jul 20, 2026
b9811ba
docs: update comments to reflect routing instead of fallback
dayana-j Jul 20, 2026
94f2d6c
fix: make integration test base classes self-contained
dayana-j Jul 20, 2026
9aae7f1
fix: lower java-client-grpc baseline from JDK 11 to JDK 8
dayana-j Jul 20, 2026
178f0bc
fix: remove explicit jackson test deps (transitive via java-client)
dayana-j Jul 20, 2026
e6bf191
fix: wire up integration tests with java21 source set
dayana-j Jul 20, 2026
69a7311
ci: add OpenSearch 3.5.0 to integration test matrix
dayana-j Jul 20, 2026
4b98fc8
fix: replace wildcard import with explicit imports (spotless)
dayana-j Jul 20, 2026
9659742
fix: remove java21 classes from unitTest task
dayana-j Jul 20, 2026
32aeab0
fix: spotless formatting for samples (license header, import order)
dayana-j Jul 20, 2026
4480aa3
fix: skip gRPC integration tests when port is unreachable
dayana-j Jul 20, 2026
70df55b
fix: only enable gRPC in testcontainer for OpenSearch 3.5.0+
dayana-j Jul 21, 2026
ae935be
ci: exclude grpc integration tests until OpenSearch 3.5.0 is released
dayana-j Jul 21, 2026
32f38c9
fix: graceful skip on container failure + re-enable 3.5.0 in CI
dayana-j Jul 21, 2026
b6b098f
fix: spotless formatting on GrpcTestContainerRule
dayana-j Jul 21, 2026
8f5a6ae
fix: remove grpcStatusToHttpStatus from FieldMappingUtil
dayana-j Jul 22, 2026
2d2b0c6
fix: skip gRPC tests early with assumeTrue for unsupported versions
dayana-j Jul 22, 2026
87ba2ff
fix: make GrpcChannelFactory methods package-private
dayana-j Jul 22, 2026
cf6e8ce
fix: apply maintainer suggestions on visibility and naming
dayana-j Jul 22, 2026
f476ce2
Merge branch 'opensearch-project:main' into grpc-java
dayana-j Jul 22, 2026
3ff92a1
Merge branch 'opensearch-project:main' into grpc-java
dayana-j Jul 29, 2026
601508e
feat: implement search over gRPC with match_all query support
dayana-j Jul 29, 2026
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
- Added `equals()` and `hashCode()` implementations to `FieldValue` ([#1998](https://github.com/opensearch-project/opensearch-java/pull/1998))
- Add document lifecycle guide and runnable sample ([#2017](https://github.com/opensearch-project/opensearch-java/pull/2017))
- Add transparent gRPC transport with HybridTransport (bulk over gRPC, REST fallback), translation layer, TLS, basic auth, AWS SigV4, and JWT support ([#2062](https://github.com/opensearch-project/opensearch-java/pull/2062))
- Add search over gRPC with match_all query support, SearchRequestConverter, SearchResponseConverter, and _source deserialization ([#2071](https://github.com/opensearch-project/opensearch-java/pull/2071))

### Fixed

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import org.opensearch.client.json.JsonpMapper;
import org.opensearch.client.opensearch.core.BulkRequest;
import org.opensearch.client.opensearch.core.BulkResponse;
import org.opensearch.client.opensearch.core.SearchRequest;
import org.opensearch.client.opensearch.core.SearchResponse;
import org.opensearch.client.transport.Endpoint;
import org.opensearch.client.transport.OpenSearchTransport;
import org.opensearch.client.transport.TransportException;
Expand All @@ -25,6 +27,7 @@
import org.opensearch.client.transport.grpc.translation.BulkResponseConverter;
import org.opensearch.client.transport.grpc.translation.GrpcStatusConverter;
import org.opensearch.protobufs.services.DocumentServiceGrpc;
import org.opensearch.protobufs.services.SearchServiceGrpc;

/**
* Pure gRPC transport for OpenSearch. Implements {@link OpenSearchTransport} and routes
Expand Down Expand Up @@ -52,7 +55,8 @@ public class GrpcTransport implements OpenSearchTransport {
static {
java.util.Set<Endpoint<?, ?, ?>> endpoints = new java.util.HashSet<>();
endpoints.add(BulkRequest._ENDPOINT);
// Future: SearchRequest._ENDPOINT, KnnSearchRequest._ENDPOINT
endpoints.add(SearchRequest._ENDPOINT);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks @dayana-j , I think this change needs a bit more thoughts:

  • the GRPC transport support is strictly endpoint base (for now), however the _search is limited to only match_all query (it is not enough to filter endpoint, we need params or/and payload)
  • the hybrid transport does internal routing based on endpoints as well, so we basically end up with the situation that user could only invoke match_all like queries even if REST based transport supports everything else

That would significantly limit its applicability I believe. The suggestion I have is to enhance endpoint support with request argument GrpcTransport.isEndpointSupported(endpoint, request) so the GRPC transport could check endpoint and perform the request conversion (yes, could be a bit wasteful work but at least we will properly route to REST transport till all queries are covered).

Curious WDYT? Thank you.

// Future: KnnSearchRequest._ENDPOINT
SUPPORTED_ENDPOINTS = java.util.Collections.unmodifiableSet(endpoints);
}

Expand All @@ -67,6 +71,7 @@ public static boolean isEndpointSupported(Endpoint<?, ?, ?> endpoint) {

private final ManagedChannel channel;
private final DocumentServiceGrpc.DocumentServiceBlockingStub documentStub;
private final SearchServiceGrpc.SearchServiceBlockingStub searchStub;
private final JsonpMapper jsonpMapper;
private final GrpcTransportOptions grpcOptions;
private final TransportOptions transportOptions;
Expand All @@ -81,6 +86,7 @@ public static boolean isEndpointSupported(Endpoint<?, ?, ?> endpoint) {
) {
this.channel = channel;
this.documentStub = channel != null ? DocumentServiceGrpc.newBlockingStub(channel) : null;
this.searchStub = channel != null ? SearchServiceGrpc.newBlockingStub(channel) : null;
this.jsonpMapper = jsonpMapper;
this.grpcOptions = grpcOptions;
this.transportOptions = transportOptions;
Expand Down Expand Up @@ -113,6 +119,9 @@ public <RequestT, ResponseT, ErrorT> ResponseT performRequest(
if (endpoint == BulkRequest._ENDPOINT) {
return (ResponseT) performBulk((BulkRequest) request);
}
if (endpoint == SearchRequest._ENDPOINT) {
return (ResponseT) performSearch((SearchRequest) request);
}

throw new UnsupportedOperationException("Endpoint registered but no handler: " + endpoint.requestUrl(request));
}
Expand Down Expand Up @@ -238,6 +247,31 @@ private BulkResponse performBulk(BulkRequest request) throws TransportException
}
}

@SuppressWarnings("unchecked")
private <TDocument> SearchResponse<TDocument> performSearch(SearchRequest request) throws TransportException {
// Convert client request to protobuf
org.opensearch.protobufs.SearchRequest protoRequest = org.opensearch.client.transport.grpc.translation.SearchRequestConverter
.toProto(request, jsonpMapper);

// Execute gRPC call
try {
org.opensearch.protobufs.SearchResponse protoResponse = searchStub.search(protoRequest);

// Convert response — use Object.class as default; the actual deserialization
// is handled by the endpoint's response deserializer in the transport layer
return (SearchResponse<TDocument>) org.opensearch.client.transport.grpc.translation.SearchResponseConverter.fromProto(
protoResponse,
jsonpMapper,
(Class<TDocument>) Object.class
);
} catch (StatusRuntimeException e) {
throw new TransportException(
"gRPC search request failed: " + e.getStatus().getDescription(),
new org.opensearch.client.transport.TransportException(e.getMessage(), e)
);
}
}

// ─── Builder ─────────────────────────────────────────────────────────────────

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/*
* 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.transport.grpc.translation;

import org.opensearch.client.json.JsonpMapper;
import org.opensearch.client.opensearch._types.query_dsl.MatchAllQuery;
import org.opensearch.client.opensearch._types.query_dsl.Query;
import org.opensearch.client.opensearch.core.SearchRequest;

/**
* Converts opensearch-java SearchRequest to protobuf SearchRequest.
*
* Currently supported query types:
* - match_all — matches all documents
*
* Query types not yet implemented (these can be executed over gRPC once
* the converter is extended — the server already supports them):
* - term, terms, terms_set
* - match, match_phrase, match_phrase_prefix, match_bool_prefix, multi_match
* - bool, constant_score, function_score
* - range, prefix, wildcard, regexp, fuzzy, exists, ids
* - nested, geo_distance, geo_bounding_box
* - knn, hybrid, script
*
* To add a new query type, implement a convertXxx() method and add a case
* to convertQuery(Query).
*/
public class SearchRequestConverter {

/**
* Convert an opensearch-java SearchRequest to a protobuf SearchRequest.
*
* @param request the client SearchRequest
* @param jsonpMapper the JSON mapper (for future use with script queries)
* @return the protobuf SearchRequest
*/
public static org.opensearch.protobufs.SearchRequest toProto(SearchRequest request, JsonpMapper jsonpMapper) {
org.opensearch.protobufs.SearchRequest.Builder protoBuilder = org.opensearch.protobufs.SearchRequest.newBuilder();

// Set index(es)
if (request.index() != null && !request.index().isEmpty()) {
protoBuilder.addAllIndex(request.index());
}

// Build SearchRequestBody
org.opensearch.protobufs.SearchRequestBody.Builder bodyBuilder = org.opensearch.protobufs.SearchRequestBody.newBuilder();

// Set size
if (request.size() != null) {
bodyBuilder.setSize(request.size());
}

// Set from (pagination offset)
if (request.from() != null) {
bodyBuilder.setFrom(request.from());
}

// Convert query
if (request.query() != null) {
bodyBuilder.setQuery(convertQuery(request.query()));
}

protoBuilder.setSearchRequestBody(bodyBuilder.build());
return protoBuilder.build();
}

/**
* Convert an opensearch-java Query to a protobuf QueryContainer.
*
* @throws UnsupportedOperationException if the query type is not yet supported
*/
private static org.opensearch.protobufs.QueryContainer convertQuery(Query query) {
org.opensearch.protobufs.QueryContainer.Builder containerBuilder = org.opensearch.protobufs.QueryContainer.newBuilder();

if (query.isMatchAll()) {
containerBuilder.setMatchAll(convertMatchAll(query.matchAll()));
} else {
throw new UnsupportedOperationException(
"Query type '"
+ query._kind()
+ "' is not yet supported for gRPC transport in this version. "
+ "Use REST transport for this query type."
);
}

return containerBuilder.build();
}

/**
* Convert MatchAllQuery to protobuf.
* MatchAllQuery has only optional boost and _name fields.
*/
private static org.opensearch.protobufs.MatchAllQuery convertMatchAll(MatchAllQuery matchAll) {
org.opensearch.protobufs.MatchAllQuery.Builder builder = org.opensearch.protobufs.MatchAllQuery.newBuilder();

if (matchAll.boost() != null) {
builder.setBoost(matchAll.boost().floatValue());
}

return builder.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/*
* 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.transport.grpc.translation;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import org.opensearch.client.json.JsonpMapper;
import org.opensearch.client.opensearch._types.ShardStatistics;
import org.opensearch.client.opensearch.core.SearchResponse;
import org.opensearch.client.opensearch.core.search.Hit;
import org.opensearch.client.opensearch.core.search.HitsMetadata;
import org.opensearch.client.opensearch.core.search.TotalHits;
import org.opensearch.client.opensearch.core.search.TotalHitsRelation;

/**
* Converts protobuf SearchResponse to opensearch-java SearchResponse format.
* <p>
* Currently supports match_all queries. Other query types (term, match, bool, etc.)
* are not yet implemented in the client-side converter but can be executed over gRPC
* once the request converter supports them — the response format is the same regardless
* of query type.
* <p>
* Note: Unsupported query types will throw UnsupportedOperationException at the request
* conversion stage. The response converter handles all response formats since the
* SearchResponse structure is query-agnostic.
*/
public class SearchResponseConverter {

/**
* Convert a protobuf SearchResponse to an opensearch-java SearchResponse.
*
* @param protoResponse the protobuf SearchResponse from the server
* @param jsonpMapper the JSON mapper for _source deserialization
* @param tDocumentClass the target class for hit _source deserialization
* @param <TDocument> the document type
* @return the opensearch-java SearchResponse
*/
public static <TDocument> SearchResponse<TDocument> fromProto(
org.opensearch.protobufs.SearchResponse protoResponse,
JsonpMapper jsonpMapper,
Class<TDocument> tDocumentClass
) {
SearchResponse.Builder<TDocument> builder = new SearchResponse.Builder<TDocument>();

// took
builder.took(protoResponse.getTook());

// timed_out
builder.timedOut(protoResponse.getTimedOut());

// _shards
if (protoResponse.hasXShards()) {
org.opensearch.protobufs.ShardStatistics protoShards = protoResponse.getXShards();
builder.shards(
new ShardStatistics.Builder().total(protoShards.getTotal())
.successful(protoShards.getSuccessful())
.failed(protoShards.getFailed())
.build()
);
} else {
builder.shards(new ShardStatistics.Builder().total(0).successful(0).failed(0).build());
}

// hits
if (protoResponse.hasHits()) {
builder.hits(convertHitsMetadata(protoResponse.getHits(), jsonpMapper, tDocumentClass));
} else {
builder.hits(new HitsMetadata.Builder<TDocument>().hits(new ArrayList<>()).build());
}

return builder.build();
}

private static <TDocument> HitsMetadata<TDocument> convertHitsMetadata(
org.opensearch.protobufs.HitsMetadata protoHits,
JsonpMapper jsonpMapper,
Class<TDocument> tDocumentClass
) {
HitsMetadata.Builder<TDocument> builder = new HitsMetadata.Builder<TDocument>();

// total hits
if (protoHits.hasTotal()) {
org.opensearch.protobufs.HitsMetadataTotal protoTotal = protoHits.getTotal();
if (protoTotal.hasTotalHits()) {
org.opensearch.protobufs.TotalHits totalHits = protoTotal.getTotalHits();
TotalHitsRelation relation = totalHits.getRelation() == org.opensearch.protobufs.TotalHitsRelation.TOTAL_HITS_RELATION_EQ
? TotalHitsRelation.Eq
: TotalHitsRelation.Gte;
builder.total(new TotalHits.Builder().value(totalHits.getValue()).relation(relation).build());
}
}

// max_score
if (protoHits.hasMaxScore()) {
org.opensearch.protobufs.HitsMetadataMaxScore maxScore = protoHits.getMaxScore();
if (maxScore.hasFloat()) {
builder.maxScore(maxScore.getFloat());
}
}

// individual hits
List<Hit<TDocument>> hits = new ArrayList<>();
for (org.opensearch.protobufs.HitsMetadataHitsInner protoHit : protoHits.getHitsList()) {
hits.add(convertHit(protoHit, jsonpMapper, tDocumentClass));
}
builder.hits(hits);

return builder.build();
}

private static <TDocument> Hit<TDocument> convertHit(
org.opensearch.protobufs.HitsMetadataHitsInner protoHit,
JsonpMapper jsonpMapper,
Class<TDocument> tDocumentClass
) {
Hit.Builder<TDocument> builder = new Hit.Builder<TDocument>();

// _index
builder.index(protoHit.getXIndex());

// _id
builder.id(protoHit.getXId());

// _score
if (protoHit.hasXScore()) {
org.opensearch.protobufs.HitXScore score = protoHit.getXScore();
if (score.hasDouble()) {
builder.score(score.getDouble());
}
}

// _version
if (protoHit.hasXVersion()) {
builder.version(protoHit.getXVersion());
}

// _seq_no
if (protoHit.hasXSeqNo()) {
builder.seqNo(protoHit.getXSeqNo());
}

// _primary_term
if (protoHit.hasXPrimaryTerm()) {
builder.primaryTerm(protoHit.getXPrimaryTerm());
}

// _source — decode bytes and deserialize to TDocument
if (!protoHit.getXSource().isEmpty()) {
TDocument source = deserializeSource(protoHit.getXSource().toByteArray(), jsonpMapper, tDocumentClass);
builder.source(source);
}

return builder.build();
}

/**
* Decode _source bytes from protobuf hit to a Java object.
* The server returns _source as UTF-8 JSON bytes.
*/
static <TDocument> TDocument deserializeSource(byte[] sourceBytes, JsonpMapper jsonpMapper, Class<TDocument> tDocumentClass) {
InputStream stream = new ByteArrayInputStream(sourceBytes);
jakarta.json.stream.JsonParser parser = jsonpMapper.jsonProvider().createParser(stream);
parser.next(); // advance to first token
return jsonpMapper.deserialize(parser, tDocumentClass);
}
}
Loading
Loading