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_normalize → date_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_bounds → n.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.
A
range(and, before its #789 guard, aterm) on adatefield with sub-millisecond precision (≥4 fractional-second digits, e.g."2020-01-01T00:00:00.123456Z"— the default output of Pythondatetime.isoformat, GoRFC3339Nano, JavaInstant.toString) returns 0 after flush, even for a bound equal to the doc's exact stored value.Cause (scale mismatch in the sort-shadow)
sort_date_normalize→date_string_to_epoch, whoseis_nanosbranch (index.rs ~40089/40096) returns epoch-nanoseconds when the value has ≥4 fractional digits.parse_date_ms(aggs.rs:4981), which always returns epoch-milliseconds.build_range_prefilter_cached(hits, ~23918) andshadow_range_count/try_shortcut_count(size:0 count, ~21205) compare the ms bound (shadow_range_bounds→n.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
rangeon a sub-ms date: wrong (0) after flush.termon a sub-ms date: fix(engine): #788 a term on a date field matches by instant, across every path #789 now side-steps this by leaving sub-msterms an exact literal lookup (which matches), rather than rewriting them to an ms-scale range. That is a targeted guard, not a fix of the underlying mismatch.Fix direction
Make the shadow bound and the shadow key agree on scale: either have
shadow_range_boundsencode the bound with the samedate_string_to_epochscale 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_aliasesrecurses only intoBoolchildren, so a CIDR (#782) or date (#788)termnested inconstant_score/function_score/dis_max/nested/boosted/named/pinnedis not rewritten and still returns the pre-rewrite result. Worth extending the recursion to the score-wrappers if it matters.