Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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))

### Dependencies
- Bump `org.apache.httpcomponents.client5:httpclient5` from 5.6 to 5.6.1 ([#1967](https://github.com/opensearch-project/opensearch-java/pull/1967))
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);
// Future: KnnSearchRequest._ENDPOINT
SUPPORTED_ENDPOINTS = java.util.Collections.unmodifiableSet(endpoints);
}

Expand All @@ -63,10 +67,35 @@ public static boolean isEndpointSupported(Endpoint<?, ?, ?> endpoint) {
return SUPPORTED_ENDPOINTS.contains(endpoint);
}

/**
* Returns true if the given endpoint and request can be handled by gRPC transport.
* For endpoints that only partially support gRPC (e.g., search with limited query types),
* this method inspects the request to determine if gRPC can handle it.
*/
public static <RequestT> boolean isEndpointSupported(Endpoint<?, ?, ?> endpoint, RequestT request) {
if (!SUPPORTED_ENDPOINTS.contains(endpoint)) {
return false;
}
// Bulk: all operations supported
if (endpoint == BulkRequest._ENDPOINT) {
return true;
}
// Search: only match_all is currently supported
if (endpoint == SearchRequest._ENDPOINT && request instanceof SearchRequest) {
SearchRequest searchRequest = (SearchRequest) request;
if (searchRequest.query() == null) {
return true; // No query = match_all by default
}
return searchRequest.query().isMatchAll();
}
return true;
}

// ─── Instance Fields ─────────────────────────────────────────────────────────

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 +110,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 All @@ -101,7 +131,7 @@ public <RequestT, ResponseT, ErrorT> ResponseT performRequest(
@Nullable TransportOptions options
) throws IOException {

if (!GrpcTransport.isEndpointSupported(endpoint)) {
if (!GrpcTransport.isEndpointSupported(endpoint, request)) {
throw new UnsupportedOperationException(
"Endpoint not supported by gRPC transport: "
+ endpoint.requestUrl(request)
Expand All @@ -113,6 +143,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 +271,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
Expand Up @@ -58,8 +58,8 @@ public <RequestT, ResponseT, ErrorT> ResponseT performRequest(
@Nullable TransportOptions options
) throws IOException {

// Route unsupported endpoints directly to REST
if (!GrpcTransport.isEndpointSupported(endpoint)) {
// Route unsupported endpoints/requests directly to REST
if (!GrpcTransport.isEndpointSupported(endpoint, request)) {
return restTransport.performRequest(request, endpoint, options);
}

Expand All @@ -73,8 +73,8 @@ public <RequestT, ResponseT, ErrorT> CompletableFuture<ResponseT> performRequest
Endpoint<RequestT, ResponseT, ErrorT> endpoint,
@Nullable TransportOptions options
) {
// Route unsupported endpoints directly to REST
if (!GrpcTransport.isEndpointSupported(endpoint)) {
// Route unsupported endpoints/requests directly to REST
if (!GrpcTransport.isEndpointSupported(endpoint, request)) {
return restTransport.performRequestAsync(request, endpoint, options);
}

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();
}
}
Loading
Loading