Return a partial result with a warning instead of exhausting PIT contexts on a text/keyword mapping conflict - #5657
Conversation
Introduce a first-class warnings array on the query response so the engine can attach non-fatal, structured notices (type/message/detail) to an otherwise-successful result, without turning it into an error. This is the prerequisite for returning a labeled partial result instead of failing a query that would otherwise exhaust Point-In-Time contexts. - Add Warning value type and QueryResponse.warnings (core). - Collect warnings during Calcite planning via a thread-local on CalcitePlanContext, drained in OpenSearchExecutionEngine.buildResultSet and cleared with the other lifecycle signals so nothing leaks across queries. - Thread warnings through QueryResult and emit them in SimpleJsonResponseFormatter only when non-empty, so existing responses are byte-for-byte unchanged. Scoped to the Calcite PPL path. Signed-off-by: Kai Huang <ahkcs@amazon.com>
When an aggregation groups by a field that a text/keyword mapping conflict collapsed to text-without-keyword across a wildcard pattern, pushdown fails and the query falls back to a per-shard document scan that opens a Point-In-Time context on every shard, tripping search.max_open_pit_context. Add an opt-in fallback: when normal aggregate pushdown fails and plugins.calcite.partial_result.on_mapping_conflict.enabled is set, narrow the scan to the largest homogeneous subset of indices whose mapping of the grouped field is aggregatable, push the aggregation down over just that subset (size=0, no PIT), and attach a PARTIAL_RESULT warning naming the excluded indices. - New default-off setting, registered in OpenSearchSettings. - tryPartialResultAggregate in CalciteLogicalIndexScan partitions the matched indices, keeps the keyword group (or text-with-keyword if no keyword group), rebinds the pushdown context to the narrowed index preserving pushed operations such as a WHERE filter, and re-runs pushdown. - Wired into AggregateIndexScanRule as the fallback when pushdown returns null. - drainWarnings de-duplicates, since the planner may raise the warning for multiple equivalent plan alternatives. - Integration test verifies: partial off fails with PIT exhaustion; partial on returns the keyword-index counts with the warning and no PIT; a conflict-free aggregation attaches no warning. Scoped to the Calcite PPL path. Signed-off-by: Kai Huang <ahkcs@amazon.com>
A partial result is only safe if the response can carry the warning that says so. Refuse partial mode (fall through to the normal path) when the requested format has no warnings channel -- CSV, RAW, and VIZ -- so a knowingly-partial result is never returned silently. - QueryContext carries a warnings-supported flag, set in TransportPPLQueryAction from the request format (true only for the JSON shape), read by the producer. - tryPartialResultAggregate bails when warnings are unsupported. - Integration test: partial on + format=csv still errors on PIT exhaustion rather than returning a silently-partial CSV. Signed-off-by: Kai Huang <ahkcs@amazon.com>
The partial-result partitioner looked up the grouped field in each index's raw field mappings by its dotted name. A nested/object field such as resource.attributes.applicationid is stored as an object tree, not a flat dotted key, so the lookup returned null, every index classified as NOT_AGGREGATABLE, and the producer bailed -- leaving the query to exhaust PIT contexts. This is the exact shape of the real observability field that motivated the feature. Flatten each index's field mappings with OpenSearchDataType.traverseAndFlatten (the same flattening the field-type resolver uses) before the lookup, so the dotted bucket name resolves. Add an integration test over a nested-field conflict pattern. Found by live testing the customer query; the flat-field integration test missed it. Signed-off-by: Kai Huang <ahkcs@amazon.com>
Two refinements to the partial-result partitioner:
- Pick the kept index group by a deterministic priority instead of a
count-based majority: always keep the keyword group when any keyword index
exists (the canonical aggregatable representation), fall back to the
text-with-.keyword group only when there is no keyword index, and always
exclude bare-text. The returned data no longer depends on how many indices
of each type match, so a stray index can't flip which subset the user sees.
- Correct the warning wording: the old remedy ("add a .keyword sub-field")
was misleading when the excluded index already had one. Reword to say the
aggregation ran over the largest consistently-mapped subset and to suggest
aligning the mapping (e.g. keyword everywhere).
Add an integration test where keyword is outnumbered 2:1 by text-with-.keyword
indices and must still be the kept group.
Signed-off-by: Kai Huang <ahkcs@amazon.com>
PR Reviewer Guide 🔍(Review updated until commit 078c949)Here are some key observations to aid the review process:
|
PR Code Suggestions ✨Latest suggestions up to 078c949 Explore these optional code suggestions:
Previous suggestionsSuggestions up to commit dad3bb3
Suggestions up to commit b475c5b
|
A wide observability pattern can exclude hundreds of indices; listing them all verbatim in the warning detail produces an unreadable multi-kilobyte message. The exact count is already in the warning's summary, so spell out at most a few excluded index names in the detail and summarize the rest as "... and N more". Add an integration test with a large excluded set asserting the detail is truncated while the summary still reports the full count. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit dad3bb3 |
The detail said the aggregation ran over the 'largest consistently-mapped subset', leftover from when the kept group was chosen by index count. Selection is now by whether the field is aggregatable there (keyword-first), not size, so reword to 'ran only over the indices where the field is aggregatable'. Signed-off-by: Kai Huang <ahkcs@amazon.com>
|
Persistent review updated to latest commit 078c949 |
Harden the partial-result fallback from POC-shaped code into a testable unit: - Move the classify/partition/priority/warning logic out of the 600-line CalciteLogicalIndexScan into a dedicated PartialResultAggregatePushdown. The scan's tryPartialResultAggregate keeps only the plan-time wiring (settings gate, mapping lookup, narrowed-scan construction, warning emission) and delegates the decision to PartialResultAggregatePushdown.plan(...). - Make the PARTIAL_RESULT warning type a shared constant (Warning.TYPE_PARTIAL_RESULT) instead of a literal, since consumers such as OpenSearch Dashboards branch on it -- a cross-surface contract. - Add a field-map constructor to IndexMapping for testability. - Add unit tests covering classification (keyword / text+keyword / bare-text / absent), multi-field weakest-resolution, the keyword-first priority ladder (including when keyword is outnumbered), null/no-op cases, excluded-list sorting, and warning-list truncation. No behavior change; the integration tests are unchanged and still pass. Signed-off-by: Kai Huang <ahkcs@amazon.com>
Description
On the Calcite PPL path, an aggregation (
stats/top/rare) that groups, sorts, or dedups on a field whose mapping conflicts across a wildcard index pattern —keywordin some indices,textin others — cannot push down. The type merge must pick one type valid on every shard, so it collapses the field totext-without-.keyword; text has no doc values, so the aggregation falls back to an in-process document scan that opens a Point-In-Time (PIT) context on every shard of every matched index. On a wide pattern this tripssearch.max_open_pit_contextand the customer sees an opaque failure.This PR adds an opt-in fallback that returns a partial result plus a structured warning instead of failing. When normal aggregate pushdown fails on such a conflict and the setting is enabled, the engine narrows the scan to the subset of indices where the field is aggregatable, pushes the aggregation down over just that subset (so
size = 0and no PIT is opened), and attaches aPARTIAL_RESULTwarning naming the excluded indices.Two parts:
warningsarray ({type, message, detail}), emitted only when non-empty so existing responses are byte-for-byte unchanged:{ "schema": [ ... ], "datarows": [ ... ], "total": 3, "size": 3, "warnings": [ { "type": "PARTIAL_RESULT", "message": "Results exclude 1 of 2 indices due to a text/keyword mapping conflict on [applicationid].", "detail": "Field [applicationid] is mapped inconsistently ... The aggregation ran over the largest consistently-mapped subset; excluded indices: [logs-text]. Align the field's mapping across all indices (e.g. map it as keyword everywhere) to include them." } ] }AggregateIndexScanRuleas the fallback when pushdown returnsnull.Behavior (setting off vs on), on a
keyword+textwildcard pattern withsearch.max_open_pit_contextbelow the shard count:stats count() by <conflict field>PARTIAL_RESULTwarning, 0 PITstop/rare <conflict field>stats count() by span(...)(no conflict)format=csvKey design points
plugins.calcite.partial_result.on_mapping_conflict.enabled). A partial result is a knowingly-incomplete answer, so it never happens silently.warningsarray, so partial mode is skipped for CSV/RAW/VIZ — those fall through to the normal (failing) path rather than returning a silently-partial answer.keywordgroup when any keyword index exists (the canonical aggregatable representation); fall back to thetext-with-.keywordgroup only when there is no keyword index; always exclude bare-text. The returned data does not depend on how many indices of each type happen to match.Not in scope: recovering an excluded but aggregatable group (e.g.
text-with-.keywordwhen akeywordgroup is also present) — that needs a per-group split-and-union, which is a larger, separate change. Partial mode deliberately trades that completeness for a single narrowed scan and a clear warning.Related Issues
Check List
--signoffor-s.By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.