Follow-up from #876 (which fixed _lastUpdated on Postgres, #871). While checking whether the other backends needed the same fix, Elasticsearch turned out to be the one backend whose _lastUpdated clause does not go through a precision-aware date builder.
Where
build_last_updated_clause in crates/persistence/src/backends/elasticsearch/search/query_builder.rs.
It writes each SearchValue straight into a single range map on the last_updated field, keyed only by gte/gt/lt/lte. Indexed date parameters on the same backend go through parameter_handlers/date.rs, which computes a [start, end) period from the value's precision and handles every prefix. _lastUpdated bypasses that.
What is wrong
Nothing returns a 500 (unlike #871), and because ES rounds date-only range bounds itself, eq, gt, lt, ge, and le behave correctly at day/month/year precision. The gaps are:
ne, sa, eb, ap all fall into the _ => arm and are treated as eq. _lastUpdated=ne2026-09-01 returns exactly the resources updated on that day instead of everything else. sa/eb should mirror gt/lt on the whole period.
- Comma-separated OR values collapse into one map.
_lastUpdated=2026-09-01,2026-09-03 inserts gte/lte twice; the second value overwrites the first, so an OR silently becomes a single-value match.
- Inconsistent with the other backends. SQLite (
date_condition), MongoDB (build_date_filter_doc), and now Postgres (build_last_updated_condition) all produce the same period-range semantics for _lastUpdated. ES is the odd one out.
Suggested fix
Reuse the period logic from parameter_handlers/date.rs (its date_precision_range plus the per-prefix match) against the top-level last_updated field instead of the nested search_params.date.value, and emit one clause per value combined with bool.should / minimum_should_match: 1 so OR lists work. ne becomes must_not of the [start, end) range.
Tests
- Unit tests on the query builder pinning the emitted JSON for
ne, sa, eb, and a two-value OR list.
- Extend the ES integration test in
crates/persistence/tests/elasticsearch_tests.rs, which today only covers a single ge case (_lastUpdated search should find recently created resource), with a ne case that must exclude the just-created resource.
Not affected
https://claude.ai/code/session_01334x1QZSmUsznAtEVcnVy7
Follow-up from #876 (which fixed
_lastUpdatedon Postgres, #871). While checking whether the other backends needed the same fix, Elasticsearch turned out to be the one backend whose_lastUpdatedclause does not go through a precision-aware date builder.Where
build_last_updated_clauseincrates/persistence/src/backends/elasticsearch/search/query_builder.rs.It writes each
SearchValuestraight into a singlerangemap on thelast_updatedfield, keyed only bygte/gt/lt/lte. Indexed date parameters on the same backend go throughparameter_handlers/date.rs, which computes a[start, end)period from the value's precision and handles every prefix._lastUpdatedbypasses that.What is wrong
Nothing returns a 500 (unlike #871), and because ES rounds date-only range bounds itself,
eq,gt,lt,ge, andlebehave correctly at day/month/year precision. The gaps are:ne,sa,eb,apall fall into the_ =>arm and are treated aseq._lastUpdated=ne2026-09-01returns exactly the resources updated on that day instead of everything else.sa/ebshould mirrorgt/lton the whole period._lastUpdated=2026-09-01,2026-09-03insertsgte/ltetwice; the second value overwrites the first, so an OR silently becomes a single-value match.date_condition), MongoDB (build_date_filter_doc), and now Postgres (build_last_updated_condition) all produce the same period-range semantics for_lastUpdated. ES is the odd one out.Suggested fix
Reuse the period logic from
parameter_handlers/date.rs(itsdate_precision_rangeplus the per-prefix match) against the top-levellast_updatedfield instead of the nestedsearch_params.date.value, and emit one clause per value combined withbool.should/minimum_should_match: 1so OR lists work.nebecomesmust_notof the[start, end)range.Tests
ne,sa,eb, and a two-value OR list.crates/persistence/tests/elasticsearch_tests.rs, which today only covers a singlegecase (_lastUpdated search should find recently created resource), with anecase that must exclude the just-created resource.Not affected
_lastUpdated.metafix lives inStoredResourceand covers all backends.https://claude.ai/code/session_01334x1QZSmUsznAtEVcnVy7