Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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 @@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- The dynamic mapping parameter supports false_allow_templates ([#19065](https://github.com/opensearch-project/OpenSearch/pull/19065))
- Add a toBuilder method in EngineConfig to support easy modification of configs([#19054](https://github.com/opensearch-project/OpenSearch/pull/19054))
- Add StoreFactory plugin interface for custom Store implementations([#19091](https://github.com/opensearch-project/OpenSearch/pull/19091))
- Publish transport-grpc-spi exposing QueryBuilderProtoConverter and QueryBuilderProtoConverterRegistry ([#18949](https://github.com/opensearch-project/OpenSearch/pull/18949))

### Changed
- Add CompletionStage variants to methods in the Client Interface and default to ActionListener impl ([#18998](https://github.com/opensearch-project/OpenSearch/pull/18998))
Expand Down
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ woodstox = "6.4.0"
kotlin = "1.7.10"
antlr4 = "4.13.1"
guava = "33.2.1-jre"
opensearchprotobufs = "0.6.0"
protobuf = "3.25.8"
jakarta_annotation = "1.3.5"
google_http_client = "1.44.1"
Expand Down
1 change: 1 addition & 0 deletions modules/transport-grpc/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ testClusters {
}

dependencies {
api project('spi')
compileOnly "com.google.code.findbugs:jsr305:3.0.2"
runtimeOnly "com.google.guava:guava:${versions.guava}"
implementation "com.google.errorprone:error_prone_annotations:2.24.1"
Expand Down
277 changes: 277 additions & 0 deletions modules/transport-grpc/spi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
# transport-grpc-spi

Service Provider Interface (SPI) for the OpenSearch gRPC transport module. This module provides interfaces and utilities that allow external plugins to extend the gRPC transport functionality.

## Overview

The `transport-grpc-spi` module enables plugin developers to:
- Implement custom query converters for gRPC transport
- Extend gRPC protocol buffer handling
- Register custom query types that can be processed via gRPC

## Key Components

### QueryBuilderProtoConverter

Interface for converting protobuf query messages to OpenSearch QueryBuilder objects.

```java
public interface QueryBuilderProtoConverter {
QueryContainer.QueryContainerCase getHandledQueryCase();
QueryBuilder fromProto(QueryContainer queryContainer);
}
```

### QueryBuilderProtoConverterSpiRegistry

Registry that manages and discovers all available query converters. External plugins can register their custom converters through this registry.

## Usage for Plugin Developers

### 1. Add Dependency

Add the SPI dependency to your plugin's `build.gradle`:

```gradle
dependencies {
compileOnly 'org.opensearch.plugin:transport-grpc-spi:${opensearch.version}'
compileOnly 'org.opensearch:protobufs:${protobufs.version}'
}
```

### 2. Implement Custom Query Converter

```java
public class MyCustomQueryConverter implements QueryBuilderProtoConverter {

@Override
public QueryContainer.QueryContainerCase getHandledQueryCase() {
return QueryContainer.QueryContainerCase.MY_CUSTOM_QUERY;
}

@Override
public QueryBuilder fromProto(QueryContainer queryContainer) {
// Convert your custom protobuf query to QueryBuilder
MyCustomQuery customQuery = queryContainer.getMyCustomQuery();
return new MyCustomQueryBuilder(customQuery.getField(), customQuery.getValue());
}
}
```

### 3. Register Your Converter

In your plugin's main class, return the converter from createComponents:

```java
public class MyPlugin extends Plugin {

@Override
public Collection<Object> createComponents(Client client, ClusterService clusterService,
ThreadPool threadPool, ResourceWatcherService resourceWatcherService,
ScriptService scriptService, NamedXContentRegistry xContentRegistry,
Environment environment, NodeEnvironment nodeEnvironment,
NamedWriteableRegistry namedWriteableRegistry,
IndexNameExpressionResolver indexNameExpressionResolver,
Supplier<RepositoriesService> repositoriesServiceSupplier) {

// Return your converter instance - the transport-grpc plugin will discover and register it
return Collections.singletonList(new MyCustomQueryConverter());
}
}
```

**Step 3b: Create SPI Registration File**

Create a file at `src/main/resources/META-INF/services/org.opensearch.transport.grpc.spi.QueryBuilderProtoConverter`:

```
org.opensearch.mypackage.MyCustomQueryConverter
```

**Step 3c: Declare Extension in Plugin Descriptor**

In your `plugin-descriptor.properties`, declare that your plugin extends transport-grpc:

```properties
extended.plugins=transport-grpc
```

### 4. Accessing the Registry (For Complex Queries)

If your converter needs to handle nested queries (like k-NN's filter clause), you'll need access to the registry to convert other query types. The transport-grpc plugin will inject the registry into your converter.

```java
public class MyCustomQueryConverter implements QueryBuilderProtoConverter {

private QueryBuilderProtoConverterRegistry registry;

@Override
public void setRegistry(QueryBuilderProtoConverterRegistry registry) {
this.registry = registry;
}

@Override
public QueryBuilder fromProto(QueryContainer queryContainer) {
MyCustomQuery customQuery = queryContainer.getMyCustomQuery();

MyCustomQueryBuilder builder = new MyCustomQueryBuilder(
customQuery.getField(),
customQuery.getValue()
);

// Handle nested queries using the injected registry
if (customQuery.hasFilter()) {
QueryContainer filterContainer = customQuery.getFilter();
QueryBuilder filterQuery = registry.fromProto(filterContainer);
builder.filter(filterQuery);
}

return builder;
}
}
```

**Registry Injection Pattern**

**How k-NN Now Accesses Built-in Converters**:

The gRPC plugin **injects the populated registry** into converters that need it:

```java
// 1. Converter interface has a default setRegistry method
public interface QueryBuilderProtoConverter {
QueryBuilder fromProto(QueryContainer queryContainer);

default void setRegistry(QueryBuilderProtoConverterRegistry registry) {
// By default, converters don't need a registry
// Converters that handle nested queries should override this method
}
}

// 2. GrpcPlugin injects registry into loaded extensions
for (QueryBuilderProtoConverter converter : queryConverters) {
// Inject the populated registry into the converter
converter.setRegistry(queryRegistry);

// Register the converter
queryRegistry.registerConverter(converter);
}
```

**Registry Access Pattern for Converters with Nested Queries**:
```java
public class KNNQueryBuilderProtoConverter implements QueryBuilderProtoConverter {

private QueryBuilderProtoConverterRegistry registry;

@Override
public void setRegistry(QueryBuilderProtoConverterRegistry registry) {
this.registry = registry;
// Pass the registry to utility classes that need it
KNNQueryBuilderProtoUtils.setRegistry(registry);
}

@Override
public QueryBuilder fromProto(QueryContainer queryContainer) {
// The utility class can now convert nested queries using the injected registry
return KNNQueryBuilderProtoUtils.fromProto(queryContainer.getKnn());
}
}
```


## Testing

### Unit Tests

```bash
./gradlew :modules:transport-grpc:spi:test
```

### Testing Your Custom Converter

```java
@Test
public void testCustomQueryConverter() {
MyCustomQueryConverter converter = new MyCustomQueryConverter();

// Create test protobuf query
QueryContainer queryContainer = QueryContainer.newBuilder()
.setMyCustomQuery(MyCustomQuery.newBuilder()
.setField("test_field")
.setValue("test_value")
.build())
.build();

// Convert and verify
QueryBuilder result = converter.fromProto(queryContainer);
assertThat(result, instanceOf(MyCustomQueryBuilder.class));

MyCustomQueryBuilder customQuery = (MyCustomQueryBuilder) result;
assertEquals("test_field", customQuery.fieldName());
assertEquals("test_value", customQuery.value());
}
```

## Real-World Example: k-NN Plugin
See the k-NN plugin https://github.com/opensearch-project/k-NN/pull/2833/files for an example on how to use this SPI, including handling nested queries.

**1. Dependency in build.gradle:**
```gradle
compileOnly "org.opensearch.plugin:transport-grpc-spi:${opensearch.version}"
compileOnly "org.opensearch:protobufs:0.8.0"
```

**2. Converter Implementation with Registry Access:**
```java
public class KNNQueryBuilderProtoConverter implements QueryBuilderProtoConverter {

private QueryBuilderProtoConverterRegistry registry;

@Override
public void setRegistry(QueryBuilderProtoConverterRegistry registry) {
this.registry = registry;
}

@Override
public QueryContainer.QueryContainerCase getHandledQueryCase() {
return QueryContainer.QueryContainerCase.KNN;
}

@Override
public QueryBuilder fromProto(QueryContainer queryContainer) {
KnnQuery knnQuery = queryContainer.getKnn();

KNNQueryBuilder builder = new KNNQueryBuilder(
knnQuery.getField(),
knnQuery.getVectorList().toArray(new Float[0]),
knnQuery.getK()
);

// Handle nested filter query using injected registry
if (knnQuery.hasFilter()) {
QueryContainer filterContainer = knnQuery.getFilter();
QueryBuilder filterQuery = registry.fromProto(filterContainer);
builder.filter(filterQuery);
}

return builder;
}
}
```

**3. Plugin Registration:**
```java
// In KNNPlugin.createComponents()
KNNQueryBuilderProtoConverter knnQueryConverter = new KNNQueryBuilderProtoConverter();
return ImmutableList.of(knnStats, knnQueryConverter);
```

**4. SPI File:**
```
# src/main/resources/META-INF/services/org.opensearch.transport.grpc.spi.QueryBuilderProtoConverter
org.opensearch.knn.grpc.proto.request.search.query.KNNQueryBuilderProtoConverter
```

**Why k-NN needs the registry:**
The k-NN query's `filter` field is a `QueryContainer` protobuf type that can contain any query type (MatchAll, Term, Terms, etc.). The k-NN converter needs access to the registry to convert these nested queries to their corresponding QueryBuilder objects.
22 changes: 22 additions & 0 deletions modules/transport-grpc/spi/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* 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.
*/

apply plugin: 'opensearch.build'
apply plugin: 'opensearch.publish'

base {
group = 'org.opensearch.plugin'
archivesName = 'transport-grpc-spi'
}

dependencies {
api project(":server")
api "org.opensearch:protobufs:${versions.opensearchprotobufs}"

testImplementation project(":test:framework")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1675c5085e1376fd1a107b87f7e325944ab5b4dc
Loading
Loading