If you are not looking for the optimal solution but for a way to solve the problem in Spark without too much complexity, we can divide the problem into two steps:
- split the data in blocks with 5 rows each, ignoring the text length
- if the sum of the text lenghts in one block is too large, split this block into further blocks
This solution is not optimal as it produces too many batches.
Step 1 can be implemented using zipWithIndex. When creating the batch ids, we leave enough "space" for dividing the batches later. At the end of this step all rows in one block are grouped into a single list as input for step 2:
df = ...
r = df.rdd.zipWithIndex().toDF() \
.select("_1.id", "_1.text_length", "_2") \
.withColumn("batch", F.expr("cast(_2 / 5 as long)*5")) \
.withColumn("data", F.struct("id", "text_length", "batch")) \
.groupBy("batch") \
.agg(F.collect_list("data").alias("data"))
Part 2 consists mainly of an udf that checks if within one batch the maximum text length is exceeded. If so, the batch id for the following elements is increased by one. As we have skipped enough batch ids in part 1, we do not get any collisions.
def splitBatchIfNecessary(data):
text_length = 0
batch = -1
for d in data:
text_length = text_length + d.text_length
if text_length > 1000:
if batch == -1:
text_length = 0
batch = d.batch + 1
yield (d.id, d.text_length, d.batch)
else:
text_length = d.text_length
batch = batch + 1
yield (d.id, d.text_length, batch)
else:
if batch == -1:
batch = d.batch
yield (d.id, d.text_length, batch)
schema=r.schema["data"].dataType
split_udf = F.udf(splitBatchIfNecessary, schema)
r = r.withColumn("data",split_udf(F.col("data")) ) \
.selectExpr("explode(data)") \
.select("col.*")
Output:
+---+-----------+-----+
| id|text_length|batch|
+---+-----------+-----+
| 1| 500| 0|
| 2| 400| 0|
| 3| 200| 1|
| 4| 300| 1|
| 5| 100| 1|
| 6| 100| 5|
| 7| 100| 5|
| 8| 100| 5|
| 9| 100| 5|
| 10| 300| 5|
+---+-----------+-----+
Possible optimizations would be to replace zipWithIndex by zipWithUniqueIds (but getting slightly more "incomplete" batches) or using a vectorized udf.