Why does queryNode.externalCollection.useTakeForOutput default to true? take() re-reads TOS every query and hurts under concurrency #53140
Replies: 1 comment 1 reply
|
Your benchmarks clearly isolate the behavior, and your read of the execution paths is spot on. Here is the background behind the design choice and the trade-offs: 1. Design Rationale for Defaulting
|
Uh oh!
There was an error while loading. Please reload this page.
Summary
For external collections,
queryNode.externalCollection.useTakeForOutputdefaults totrue, which routes output-field fetching through thetake()path instead ofbulk_subscript. In our testing,take()appears to re-read the source files fromobject storage (TOS/S3) on every query, with no reuse across queries, whereas the
bulk_subscriptpath (i.e.useTakeForOutput=false) populates a cache and reuses it.This gap only becomes significant when the output fields are large or
variable-length — e.g. vector (
list<float>), TEXT/JSON, or other var-len columns —because that is when per-query download + decode dominates. For small fixed-width
scalar outputs the cost is negligible and the two paths look the same. In the
measurements below we use a vector field as the output to make the effect
visible. The gap is largest with large source files and, especially, under
concurrent queries against the same segments.
I'd like to understand the design rationale behind defaulting external collections
to
take(), and whether the downside below is expected or something to be improved.Environment
refreshable: truequeryNode.externalCollection.useTakeForOutput— defaulttruequeryNode.internalCollection.useTakeForOutput— defaultfalseWhat we observe
useTakeForOutput=true(external default)useTakeForOutput=falsetake()bulk_subscriptThe two behaviors diverge sharply when:
take()pulls a lot), anddownloads/decodes of overlapping column chunks; no sharing).
Questions
take()(
useTakeForOutput=true) while internal collections default tobulk_subscript(
false)?path) expected by design, or considered a known limitation?
falsethe recommended mitigation, and what are the trade-offs (e.g. moving the memory/decoding
cost to load time, local materialization, potential OOM risk)?
sharing a reader / column-chunk bytes across segments that point to the same physical
file and projection, so PreBuffer happens once and is reused across queries?
Some Experiments
Test setup: cohere 1M dataset (~3 GB on disk), single QueryNode, HNSW index,
one external collection over the source Parquet.
targetRowsPerSegmentmaps the1M rows to 4 segments by default. Same query, only the output path differs.
1. Sequential queries (single client, 3 identical searches)
Latency per phase (seconds):
bulk_subscript(useTakeForOutput=false)take(useTakeForOutput=true, default)The first search is comparable (both pay the cold cost). From the second search
on,
bulk_subscriptserves from cache in ~10 ms, whiletakere-reads andre-decodes from object storage every single time (~43 s, unchanged across
repeats). That is a ~4000x per-query gap on a warm collection.
2. Concurrent queries (5 concurrent searches × 3 rounds), take path
take()


bulk_subscript()
All reactions