|
| 1 | +# transport-grpc-spi |
| 2 | + |
| 3 | +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. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +The `transport-grpc-spi` module enables plugin developers to: |
| 8 | +- Implement custom query converters for gRPC transport |
| 9 | +- Extend gRPC protocol buffer handling |
| 10 | +- Register custom query types that can be processed via gRPC |
| 11 | + |
| 12 | +## Key Components |
| 13 | + |
| 14 | +### QueryBuilderProtoConverter |
| 15 | + |
| 16 | +Interface for converting protobuf query messages to OpenSearch QueryBuilder objects. |
| 17 | + |
| 18 | +```java |
| 19 | +public interface QueryBuilderProtoConverter { |
| 20 | + QueryContainer.QueryContainerCase getHandledQueryCase(); |
| 21 | + QueryBuilder fromProto(QueryContainer queryContainer); |
| 22 | +} |
| 23 | +``` |
| 24 | + |
| 25 | +### QueryBuilderProtoConverterRegistry |
| 26 | + |
| 27 | +Interface for accessing the query converter registry. This provides a clean abstraction for plugins that need to convert nested queries without exposing internal implementation details. |
| 28 | + |
| 29 | +## Usage for Plugin Developers |
| 30 | + |
| 31 | +### 1. Add Dependency |
| 32 | + |
| 33 | +Add the SPI dependency to your plugin's `build.gradle`: |
| 34 | + |
| 35 | +```gradle |
| 36 | +dependencies { |
| 37 | + compileOnly 'org.opensearch.plugin:transport-grpc-spi:${opensearch.version}' |
| 38 | + compileOnly 'org.opensearch:protobufs:${protobufs.version}' |
| 39 | +} |
| 40 | +``` |
| 41 | + |
| 42 | +### 2. Implement Custom Query Converter |
| 43 | + |
| 44 | +```java |
| 45 | +public class MyCustomQueryConverter implements QueryBuilderProtoConverter { |
| 46 | + |
| 47 | + @Override |
| 48 | + public QueryContainer.QueryContainerCase getHandledQueryCase() { |
| 49 | + return QueryContainer.QueryContainerCase.MY_CUSTOM_QUERY; |
| 50 | + } |
| 51 | + |
| 52 | + @Override |
| 53 | + public QueryBuilder fromProto(QueryContainer queryContainer) { |
| 54 | + // Convert your custom protobuf query to QueryBuilder |
| 55 | + MyCustomQuery customQuery = queryContainer.getMyCustomQuery(); |
| 56 | + return new MyCustomQueryBuilder(customQuery.getField(), customQuery.getValue()); |
| 57 | + } |
| 58 | +} |
| 59 | +``` |
| 60 | + |
| 61 | +### 3. Register Your Converter |
| 62 | + |
| 63 | +In your plugin's main class, return the converter from createComponents: |
| 64 | + |
| 65 | +```java |
| 66 | +public class MyPlugin extends Plugin { |
| 67 | + |
| 68 | + @Override |
| 69 | + public Collection<Object> createComponents(Client client, ClusterService clusterService, |
| 70 | + ThreadPool threadPool, ResourceWatcherService resourceWatcherService, |
| 71 | + ScriptService scriptService, NamedXContentRegistry xContentRegistry, |
| 72 | + Environment environment, NodeEnvironment nodeEnvironment, |
| 73 | + NamedWriteableRegistry namedWriteableRegistry, |
| 74 | + IndexNameExpressionResolver indexNameExpressionResolver, |
| 75 | + Supplier<RepositoriesService> repositoriesServiceSupplier) { |
| 76 | + |
| 77 | + // Return your converter instance - the transport-grpc plugin will discover and register it |
| 78 | + return Collections.singletonList(new MyCustomQueryConverter()); |
| 79 | + } |
| 80 | +} |
| 81 | +``` |
| 82 | + |
| 83 | +**Step 3b: Create SPI Registration File** |
| 84 | + |
| 85 | +Create a file at `src/main/resources/META-INF/services/org.opensearch.transport.grpc.spi.QueryBuilderProtoConverter`: |
| 86 | + |
| 87 | +``` |
| 88 | +org.opensearch.mypackage.MyCustomQueryConverter |
| 89 | +``` |
| 90 | + |
| 91 | +**Step 3c: Declare Extension in Plugin Descriptor** |
| 92 | + |
| 93 | +In your `plugin-descriptor.properties`, declare that your plugin extends transport-grpc: |
| 94 | + |
| 95 | +```properties |
| 96 | +extended.plugins=transport-grpc |
| 97 | +``` |
| 98 | + |
| 99 | +### 4. Accessing the Registry (For Complex Queries) |
| 100 | + |
| 101 | +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. |
| 102 | + |
| 103 | +```java |
| 104 | +public class MyCustomQueryConverter implements QueryBuilderProtoConverter { |
| 105 | + |
| 106 | + private QueryBuilderProtoConverterRegistry registry; |
| 107 | + |
| 108 | + @Override |
| 109 | + public void setRegistry(QueryBuilderProtoConverterRegistry registry) { |
| 110 | + this.registry = registry; |
| 111 | + } |
| 112 | + |
| 113 | + @Override |
| 114 | + public QueryBuilder fromProto(QueryContainer queryContainer) { |
| 115 | + MyCustomQuery customQuery = queryContainer.getMyCustomQuery(); |
| 116 | + |
| 117 | + MyCustomQueryBuilder builder = new MyCustomQueryBuilder( |
| 118 | + customQuery.getField(), |
| 119 | + customQuery.getValue() |
| 120 | + ); |
| 121 | + |
| 122 | + // Handle nested queries using the injected registry |
| 123 | + if (customQuery.hasFilter()) { |
| 124 | + QueryContainer filterContainer = customQuery.getFilter(); |
| 125 | + QueryBuilder filterQuery = registry.fromProto(filterContainer); |
| 126 | + builder.filter(filterQuery); |
| 127 | + } |
| 128 | + |
| 129 | + return builder; |
| 130 | + } |
| 131 | +} |
| 132 | +``` |
| 133 | + |
| 134 | +**Registry Injection Pattern** |
| 135 | + |
| 136 | +**How k-NN Now Accesses Built-in Converters**: |
| 137 | + |
| 138 | +The gRPC plugin **injects the populated registry** into converters that need it: |
| 139 | + |
| 140 | +```java |
| 141 | +// 1. Converter interface has a default setRegistry method |
| 142 | +public interface QueryBuilderProtoConverter { |
| 143 | + QueryBuilder fromProto(QueryContainer queryContainer); |
| 144 | + |
| 145 | + default void setRegistry(QueryBuilderProtoConverterRegistry registry) { |
| 146 | + // By default, converters don't need a registry |
| 147 | + // Converters that handle nested queries should override this method |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +// 2. GrpcPlugin injects registry into loaded extensions |
| 152 | +for (QueryBuilderProtoConverter converter : queryConverters) { |
| 153 | + // Inject the populated registry into the converter |
| 154 | + converter.setRegistry(queryRegistry); |
| 155 | + |
| 156 | + // Register the converter |
| 157 | + queryRegistry.registerConverter(converter); |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +**Registry Access Pattern for Converters with Nested Queries**: |
| 162 | +```java |
| 163 | +public class KNNQueryBuilderProtoConverter implements QueryBuilderProtoConverter { |
| 164 | + |
| 165 | + private QueryBuilderProtoConverterRegistry registry; |
| 166 | + |
| 167 | + @Override |
| 168 | + public void setRegistry(QueryBuilderProtoConverterRegistry registry) { |
| 169 | + this.registry = registry; |
| 170 | + // Pass the registry to utility classes that need it |
| 171 | + KNNQueryBuilderProtoUtils.setRegistry(registry); |
| 172 | + } |
| 173 | + |
| 174 | + @Override |
| 175 | + public QueryBuilder fromProto(QueryContainer queryContainer) { |
| 176 | + // The utility class can now convert nested queries using the injected registry |
| 177 | + return KNNQueryBuilderProtoUtils.fromProto(queryContainer.getKnn()); |
| 178 | + } |
| 179 | +} |
| 180 | +``` |
| 181 | + |
| 182 | + |
| 183 | +## Testing |
| 184 | + |
| 185 | +### Unit Tests |
| 186 | + |
| 187 | +```bash |
| 188 | +./gradlew :modules:transport-grpc:spi:test |
| 189 | +``` |
| 190 | + |
| 191 | +### Testing Your Custom Converter |
| 192 | + |
| 193 | +```java |
| 194 | +@Test |
| 195 | +public void testCustomQueryConverter() { |
| 196 | + MyCustomQueryConverter converter = new MyCustomQueryConverter(); |
| 197 | + |
| 198 | + // Create test protobuf query |
| 199 | + QueryContainer queryContainer = QueryContainer.newBuilder() |
| 200 | + .setMyCustomQuery(MyCustomQuery.newBuilder() |
| 201 | + .setField("test_field") |
| 202 | + .setValue("test_value") |
| 203 | + .build()) |
| 204 | + .build(); |
| 205 | + |
| 206 | + // Convert and verify |
| 207 | + QueryBuilder result = converter.fromProto(queryContainer); |
| 208 | + assertThat(result, instanceOf(MyCustomQueryBuilder.class)); |
| 209 | + |
| 210 | + MyCustomQueryBuilder customQuery = (MyCustomQueryBuilder) result; |
| 211 | + assertEquals("test_field", customQuery.fieldName()); |
| 212 | + assertEquals("test_value", customQuery.value()); |
| 213 | +} |
| 214 | +``` |
| 215 | + |
| 216 | +## Real-World Example: k-NN Plugin |
| 217 | +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. |
| 218 | + |
| 219 | +**1. Dependency in build.gradle:** |
| 220 | +```gradle |
| 221 | +compileOnly "org.opensearch.plugin:transport-grpc-spi:${opensearch.version}" |
| 222 | +compileOnly "org.opensearch:protobufs:0.8.0" |
| 223 | +``` |
| 224 | + |
| 225 | +**2. Converter Implementation with Registry Access:** |
| 226 | +```java |
| 227 | +public class KNNQueryBuilderProtoConverter implements QueryBuilderProtoConverter { |
| 228 | + |
| 229 | + private QueryBuilderProtoConverterRegistry registry; |
| 230 | + |
| 231 | + @Override |
| 232 | + public void setRegistry(QueryBuilderProtoConverterRegistry registry) { |
| 233 | + this.registry = registry; |
| 234 | + } |
| 235 | + |
| 236 | + @Override |
| 237 | + public QueryContainer.QueryContainerCase getHandledQueryCase() { |
| 238 | + return QueryContainer.QueryContainerCase.KNN; |
| 239 | + } |
| 240 | + |
| 241 | + @Override |
| 242 | + public QueryBuilder fromProto(QueryContainer queryContainer) { |
| 243 | + KnnQuery knnQuery = queryContainer.getKnn(); |
| 244 | + |
| 245 | + KNNQueryBuilder builder = new KNNQueryBuilder( |
| 246 | + knnQuery.getField(), |
| 247 | + knnQuery.getVectorList().toArray(new Float[0]), |
| 248 | + knnQuery.getK() |
| 249 | + ); |
| 250 | + |
| 251 | + // Handle nested filter query using injected registry |
| 252 | + if (knnQuery.hasFilter()) { |
| 253 | + QueryContainer filterContainer = knnQuery.getFilter(); |
| 254 | + QueryBuilder filterQuery = registry.fromProto(filterContainer); |
| 255 | + builder.filter(filterQuery); |
| 256 | + } |
| 257 | + |
| 258 | + return builder; |
| 259 | + } |
| 260 | +} |
| 261 | +``` |
| 262 | + |
| 263 | +**3. Plugin Registration:** |
| 264 | +```java |
| 265 | +// In KNNPlugin.createComponents() |
| 266 | +KNNQueryBuilderProtoConverter knnQueryConverter = new KNNQueryBuilderProtoConverter(); |
| 267 | +return ImmutableList.of(knnStats, knnQueryConverter); |
| 268 | +``` |
| 269 | + |
| 270 | +**4. SPI File:** |
| 271 | +``` |
| 272 | +# src/main/resources/META-INF/services/org.opensearch.transport.grpc.spi.QueryBuilderProtoConverter |
| 273 | +org.opensearch.knn.grpc.proto.request.search.query.KNNQueryBuilderProtoConverter |
| 274 | +``` |
| 275 | + |
| 276 | +**Why k-NN needs the registry:** |
| 277 | +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. |
0 commit comments