Skip to content

es-compat: range/term on a sub-millisecond date returns 0 after flush (sort-shadow encodes nanos, range bound is ms) #790

Description

@xerj-org

A range (and, before its #789 guard, a term) on a date field with sub-millisecond precision (≥4 fractional-second digits, e.g. "2020-01-01T00:00:00.123456Z" — the default output of Python datetime.isoformat, Go RFC3339Nano, Java Instant.toString) returns 0 after flush, even for a bound equal to the doc's exact stored value.

Cause (scale mismatch in the sort-shadow)

  • The sort-shadow (doc-values key used by the segment range fast paths) encodes a date via sort_date_normalizedate_string_to_epoch, whose is_nanos branch (index.rs ~40089/40096) returns epoch-nanoseconds when the value has ≥4 fractional digits.
  • The range bound is parsed by parse_date_ms (aggs.rs:4981), which always returns epoch-milliseconds.
  • build_range_prefilter_cached (hits, ~23918) and shadow_range_count / try_shortcut_count (size:0 count, ~21205) compare the ms bound (shadow_range_boundsn.as_f64(), ~39547) against the nanos shadow key. The doc's key (~1.5778e18 ns) sits ~6 orders of magnitude above the bound (~1.5778e12 ms), the bisect returns an empty position set, and index.rs ~17754 skips the whole segment ("Some(∅) proves no doc can contribute").

Whole-second and millisecond-precision (≤3 fractional digits) values take the ms branch on both sides, so they agree — which is why this is invisible to most tests.

Scope

Fix direction

Make the shadow bound and the shadow key agree on scale: either have shadow_range_bounds encode the bound with the same date_string_to_epoch scale logic the keys use (nanos when the column carries sub-ms precision), or make the shadow store a consistent scale (ms + a sub-ms tiebreak) so a ms bound always compares correctly. Needs care — the sort-shadow must preserve ordering for sub-ms values, so it can't simply truncate to ms.

Found via the #789 (#788 date-term) skeptic review.


Separately (low priority, pre-existing): rewrite_query_aliases recurses only into Bool children, so a CIDR (#782) or date (#788) term nested in constant_score / function_score / dis_max / nested / boosted / named / pinned is not rewritten and still returns the pre-rewrite result. Worth extending the recursion to the score-wrappers if it matters.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions