Skip to content

Commit 1ca590a

Browse files
karenyrxmsfroh
andauthored
[GRPC] Publish transport-grpc-spi for QueryBuilderProtoConverter and QueryBuilderProtoConverterRegistry (#18949)
--------- Signed-off-by: Karen Xu <[email protected]> Signed-off-by: Karen X <[email protected]> Signed-off-by: Michael Froh <[email protected]> Co-authored-by: Michael Froh <[email protected]>
1 parent c012a51 commit 1ca590a

25 files changed

+1872
-118
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
1616
- Add overload constructor for Translog to accept Channel Factory as a parameter ([#18918](https://github.com/opensearch-project/OpenSearch/pull/18918))
1717
- Add subdirectory-aware store module with recovery support ([#19132](https://github.com/opensearch-project/OpenSearch/pull/19132))
1818
- Add a dynamic cluster setting to control the enablement of the merged segment warmer ([#18929](https://github.com/opensearch-project/OpenSearch/pull/18929))
19+
- Publish transport-grpc-spi exposing QueryBuilderProtoConverter and QueryBuilderProtoConverterRegistry ([#18949](https://github.com/opensearch-project/OpenSearch/pull/18949))
1920
- Support system generated search pipeline. ([#19128](https://github.com/opensearch-project/OpenSearch/pull/19128))
2021

2122
### Changed

gradle/libs.versions.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ woodstox = "6.4.0"
2121
kotlin = "1.7.10"
2222
antlr4 = "4.13.1"
2323
guava = "33.2.1-jre"
24+
opensearchprotobufs = "0.6.0"
2425
protobuf = "3.25.8"
2526
jakarta_annotation = "1.3.5"
2627
google_http_client = "1.44.1"

modules/transport-grpc/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ testClusters {
2121
}
2222

2323
dependencies {
24+
api project('spi')
2425
compileOnly "com.google.code.findbugs:jsr305:3.0.2"
2526
runtimeOnly "com.google.guava:guava:${versions.guava}"
2627
implementation "com.google.errorprone:error_prone_annotations:2.24.1"
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
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.
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
*
4+
* The OpenSearch Contributors require contributions made to
5+
* this file be licensed under the Apache-2.0 license or a
6+
* compatible open source license.
7+
*/
8+
9+
apply plugin: 'opensearch.build'
10+
apply plugin: 'opensearch.publish'
11+
12+
base {
13+
group = 'org.opensearch.plugin'
14+
archivesName = 'transport-grpc-spi'
15+
}
16+
17+
dependencies {
18+
api project(":server")
19+
api "org.opensearch:protobufs:${versions.opensearchprotobufs}"
20+
21+
testImplementation project(":test:framework")
22+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
1675c5085e1376fd1a107b87f7e325944ab5b4dc

0 commit comments

Comments
 (0)