Skip to content

Row extraction pages Elasticsearch sequentially and does not use shard parallelism #238

Description

@fupelaqu

Summary

Large row-returning extractions page Elasticsearch strictly sequentially, and the request shape
makes the cost grow with the shard count. On a multi-shard index the reader never uses the
parallelism the cluster is offering: neither the sidecar nor Elasticsearch is anywhere near
saturated, yet the wall clock does not improve when shards and nodes are added.

Where it is

pitSearchAfter (es8/java/.../JavaClientApi.scala:1517, and the es7/es9 equivalents) builds a
single Source.unfoldAsync: fetch a page, await it, use its sort values as search_after, fetch the
next. There is no slice and no shard preference anywhere in the client. A PIT covers the whole
index, so every page fans out to all shards and the coordinating node performs a sorted merge
before returning it.

For a 10M-row extraction at the default page size of 1,000, that is:

  • 10,000 sequential round-trips, each one a fan-out + merge across every shard
  • on a 6-shard index, roughly 60,000 shard-level operations for a single statement
  • per-page cost that rises with shard count, because both the fan-out and the cross-node gather
    grow with it

Measurements

10M rows / 8 columns, no ORDER BY, no LIMIT; 6-shard index on a 3-node Elasticsearch cluster
(3 x 2 CPU); sidecar 4 CPU / 4 GB. Medians of 3 runs after 2 warm-ups.

metric value
wall 34.6 s (≈ 3.46 ms per page)
sidecar CPU 19.3 s = 0.56 of 4 cores
Elasticsearch CPU 53.6 s = 1.55 of 6 cores
inter-node transport 2,293 MB

Neither side is CPU-bound: the pipeline is latency-bound, idle while waiting on round-trips.
The same statement against a 1-shard index on the same host takes essentially the same wall
(34.6 s), i.e. adding shards and nodes bought nothing — while the fan-out per page, the cluster
CPU and the cross-node transport all grew.

Proposal — one slice per shard, merged into one stream

Elasticsearch supports slicing on PIT + search_after. Open one PIT, start N readers sharing it,
each pinned to one slice, and merge them into the single Arrow/row stream the caller already
receives. No API or protocol change; callers still see one result.

The hard constraint

Slice count must equal shard count. With more slices than shards, Elasticsearch applies a
document-level hash filter and each slice re-scans its shard. Measured on this cluster: 5 slices over
a 1-shard index cost 84.9 s of cluster CPU, against 28.2 s for 5 slices over 5 shards
where each slice maps to exactly one shard. Choosing the slice count wrong turns this improvement
into a 3x regression in cluster CPU.

Expected effect (projection from the numbers above)

Pages per slice fall to ~1,667, so the Elasticsearch-side contribution drops to roughly 6 s and the
single-threaded row-to-Arrow conversion (19.3 s CPU) becomes the bottleneck — about 20 s wall.
Parallelising conversion per slice as well puts the floor near 10-12 s. Cluster CPU should also fall,
because the per-page fan-out and merge disappear.

Scope

In scope (first cut): the no-ORDER BY, no-LIMIT row-returning path — the one SearchApi
already routes through scroll (#209).

Out of scope initially, and why:

Risks

Cheaper mitigation to measure first

Raising the page size (1,000 -> 5,000) cuts round-trips 5x with no architectural change and no
correctness risk beyond memory. Worth quantifying before committing to slicing, and the two compose.

Acceptance criteria

  1. A no-ORDER BY extraction from an N-shard index opens N slices under one PIT and merges them.
  2. Slice count is derived from the index's shard count, never configured above it.
  3. Multi-shard completeness test: exact count and distinct ids, on real Elasticsearch, for
    1 / 3 / 6 shards.
  4. Peak sidecar memory does not regress against the sequential path.
  5. The PIT is closed exactly once on completion, failure and cancellation.
  6. ORDER BY and explicit LIMIT statements continue to use the sequential path unchanged.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions