Skip to content

Commit c782307

Browse files
committed
Enable parallel reads for OpenSearch Serverless via PIT + Slice
Remove the validation that rejected opensearch.input.max.docs.per.partition in serverless mode. When this setting is provided, the connector now creates multiple sliced partitions per index using PIT + Slice, enabling parallel reads across Spark tasks. This leverages the sliced search support introduced in next-generation OpenSearch Serverless. On Classic Serverless (which does not support the Slice API), using this setting will result in a server-side error. Changes: - RestService.findServerlessPartitions(): count docs and create sliced partitions when maxDocsPerPartition is set - SearchRequestBuilder.assembleSearchAfterBody(): include slice parameter in search_after request body - ServerlessModeTest: replace rejection test with parallel partition tests - USER_GUIDE.md: add Serverless documentation section Signed-off-by: Sotaro Hikita <bering1814@gmail.com>
1 parent 727416a commit c782307

5 files changed

Lines changed: 101 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ Inspired from [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
2121
- Add Apache Spark 4.0 support ([#684](https://github.com/opensearch-project/opensearch-hadoop/pull/684))
2222
- Add dedicated `opensearch.search_after.size` setting for serverless mode page size ([#695](https://github.com/opensearch-project/opensearch-hadoop/pull/695))
2323
- Add Apache Spark 3.5 support with dedicated opensearch-spark-35 module ([#717](https://github.com/opensearch-project/opensearch-hadoop/pull/717))
24+
- Add parallel read support for next-generation OpenSearch Serverless via PIT + Slice ([#783](https://github.com/opensearch-project/opensearch-hadoop/pull/783))
2425

2526
### Changed
2627
- Switched to more reliable OpenSearch Lucene snapshot location ([#597](https://github.com/opensearch-project/opensearch-hadoop/pull/597))

USER_GUIDE.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,3 +471,34 @@ TBLPROPERTIES(
471471
'opensearch.aws.sigv4.enabled' = 'true',
472472
'opensearch.aws.sigv4.region' = 'us-east-1');
473473
```
474+
475+
## Amazon OpenSearch Serverless
476+
477+
The connector supports Amazon OpenSearch Serverless (both Classic and NextGen architectures). Serverless collections do not expose shard information, so the connector uses PIT (Point in Time) with search_after for pagination instead of the scroll API.
478+
479+
### Basic Configuration
480+
481+
```
482+
opensearch.serverless=true
483+
opensearch.nodes=https://<collection-id>.aoss.<region>.on.aws
484+
opensearch.port=443
485+
opensearch.net.ssl=true
486+
opensearch.nodes.wan.only=true
487+
opensearch.aws.sigv4.enabled=true
488+
opensearch.aws.sigv4.region=<region>
489+
opensearch.aws.sigv4.service.name=aoss
490+
```
491+
492+
### Parallel Reads (NextGen Only)
493+
494+
By default, serverless mode reads each index serially in a single partition. The next-generation OpenSearch Serverless architecture supports PIT with sliced search, which enables parallel reads across multiple Spark tasks.
495+
496+
To enable parallel reads, set `opensearch.input.max.docs.per.partition`:
497+
498+
```
499+
opensearch.input.max.docs.per.partition=100000
500+
```
501+
502+
The connector will count the documents in each index, divide by this value to determine the number of slices, and create one Spark partition per slice. For example, an index with 1,000,000 documents and `max.docs.per.partition=100000` will produce 10 parallel read tasks.
503+
504+
This setting requires the next-generation OpenSearch Serverless architecture. Using it with Classic Serverless collections will result in a server-side error because Classic does not support the Slice API.

mr/src/main/java/org/opensearch/hadoop/rest/RestService.java

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -229,15 +229,11 @@ public static List<PartitionDefinition> findPartitions(Settings settings, Log lo
229229

230230
/**
231231
* Create partitions for OpenSearch Serverless mode, which doesn't support shard APIs.
232-
* This function creates a single partition for each index as serverless doesn't expose shard information.
232+
* When maxDocsPerPartition is set, creates multiple sliced partitions per index for parallel reads.
233+
* This requires PIT + Slice support (available on next-generation OpenSearch Serverless).
234+
* On Classic Serverless, using maxDocsPerPartition will result in a server-side error.
233235
*/
234236
static List<PartitionDefinition> findServerlessPartitions(RestRepository client, Settings settings, MappingSet mappingSet, Log log) {
235-
if (settings.getMaxDocsPerPartition() != null) {
236-
throw new OpenSearchHadoopIllegalArgumentException(
237-
"maxDocsPerPartition setting is not supported in OpenSearch Serverless mode. " +
238-
"Serverless does not support Slice API which is required for parallel partition reads.");
239-
}
240-
241237
Resource readResource = new Resource(settings, true);
242238
Mapping resolvedMapping = mappingSet == null ? null : mappingSet.getResolvedView();
243239
PartitionDefinition.PartitionDefinitionBuilder partitionBuilder = PartitionDefinition.builder(settings, resolvedMapping);
@@ -247,9 +243,22 @@ static List<PartitionDefinition> findServerlessPartitions(RestRepository client,
247243
List<PartitionDefinition> partitions = new ArrayList<PartitionDefinition>();
248244
String[] indices = readResource.index().split(",");
249245

246+
Integer maxDocsPerPartition = settings.getMaxDocsPerPartition();
247+
250248
for (String indexName : indices) {
251249
indexName = indexName.trim();
252-
partitions.add(partitionBuilder.build(indexName, 0, new String[0]));
250+
if (maxDocsPerPartition != null) {
251+
QueryBuilder query = QueryUtils.parseQueryAndFilters(settings);
252+
long numDocs = client.getRestClient().count(indexName, query);
253+
int numPartitions = (int) Math.max(1, numDocs / maxDocsPerPartition);
254+
log.info(String.format("Serverless parallel read: index [%s] has [%d] docs, creating [%d] sliced partitions", indexName, numDocs, numPartitions));
255+
for (int i = 0; i < numPartitions; i++) {
256+
PartitionDefinition.Slice slice = new PartitionDefinition.Slice(i, numPartitions);
257+
partitions.add(partitionBuilder.build(indexName, 0, slice, new String[0]));
258+
}
259+
} else {
260+
partitions.add(partitionBuilder.build(indexName, 0, new String[0]));
261+
}
253262
}
254263

255264
return partitions;

mr/src/main/java/org/opensearch/hadoop/rest/SearchRequestBuilder.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,15 @@ private BytesArray assembleSearchAfterBody() {
377377
JacksonJsonGenerator generator = new JacksonJsonGenerator(out);
378378
try {
379379
generator.writeBeginObject();
380+
if (slice != null && slice.max > 1) {
381+
generator.writeFieldName("slice");
382+
generator.writeBeginObject();
383+
generator.writeFieldName("id");
384+
generator.writeNumber(slice.id);
385+
generator.writeFieldName("max");
386+
generator.writeNumber(slice.max);
387+
generator.writeEndObject();
388+
}
380389
generator.writeFieldName("query");
381390
generator.writeBeginObject();
382391
root.toJson(generator);

mr/src/test/java/org/opensearch/hadoop/rest/ServerlessModeTest.java

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import org.apache.commons.logging.impl.NoOpLog;
1515
import org.junit.Test;
1616
import org.mockito.Mockito;
17-
import org.opensearch.hadoop.OpenSearchHadoopIllegalArgumentException;
1817
import org.opensearch.hadoop.cfg.ConfigurationOptions;
1918
import org.opensearch.hadoop.cfg.PropertiesSettings;
2019
import org.opensearch.hadoop.cfg.Settings;
@@ -79,14 +78,54 @@ public void testFindServerlessPartitionsMultipleIndices() {
7978
assertEquals("index3", partitions.get(2).getIndex());
8079
}
8180

82-
@Test(expected = OpenSearchHadoopIllegalArgumentException.class)
83-
public void testFindServerlessPartitionsRejectsMaxDocsPerPartition() {
81+
@Test
82+
public void testFindServerlessPartitionsWithMaxDocsPerPartition() {
8483
Settings settings = new PropertiesSettings();
8584
settings.setProperty(ConfigurationOptions.OPENSEARCH_RESOURCE_READ, "test-index");
8685
settings.setServerlessMode(true);
86+
settings.setMaxDocsPerPartition(500);
87+
88+
RestClient restClient = Mockito.mock(RestClient.class);
89+
Mockito.when(restClient.count(Mockito.eq("test-index"), Mockito.any()))
90+
.thenReturn(2000L);
91+
92+
RestRepository repository = Mockito.mock(RestRepository.class);
93+
Mockito.when(repository.getRestClient()).thenReturn(restClient);
94+
95+
List<PartitionDefinition> partitions = RestService.findServerlessPartitions(repository, settings, null, LOGGER);
96+
97+
assertEquals(4, partitions.size());
98+
for (int i = 0; i < 4; i++) {
99+
assertEquals("test-index", partitions.get(i).getIndex());
100+
assertNotNull(partitions.get(i).getSlice());
101+
assertEquals(i, partitions.get(i).getSlice().id);
102+
assertEquals(4, partitions.get(i).getSlice().max);
103+
}
104+
}
105+
106+
@Test
107+
public void testFindServerlessPartitionsWithMaxDocsPerPartitionMultipleIndices() {
108+
Settings settings = new PropertiesSettings();
109+
settings.setProperty(ConfigurationOptions.OPENSEARCH_RESOURCE_READ, "index1,index2");
110+
settings.setServerlessMode(true);
87111
settings.setMaxDocsPerPartition(1000);
88112

89-
RestService.findServerlessPartitions(null, settings, null, LOGGER);
113+
RestClient restClient = Mockito.mock(RestClient.class);
114+
Mockito.when(restClient.count(Mockito.eq("index1"), Mockito.any()))
115+
.thenReturn(3000L);
116+
Mockito.when(restClient.count(Mockito.eq("index2"), Mockito.any()))
117+
.thenReturn(1500L);
118+
119+
RestRepository repository = Mockito.mock(RestRepository.class);
120+
Mockito.when(repository.getRestClient()).thenReturn(restClient);
121+
122+
List<PartitionDefinition> partitions = RestService.findServerlessPartitions(repository, settings, null, LOGGER);
123+
124+
// index1: 3000/1000 = 3 partitions, index2: 1500/1000 = 1 partition
125+
assertEquals(4, partitions.size());
126+
assertEquals("index1", partitions.get(0).getIndex());
127+
assertEquals("index1", partitions.get(2).getIndex());
128+
assertEquals("index2", partitions.get(3).getIndex());
90129
}
91130

92131
@Test

0 commit comments

Comments
 (0)