Skip to content

feat(search)!: declare date facet bins in ISO 8601, with max covering its whole period - #991

Merged
ddeboer merged 2 commits into
mainfrom
feat/date-facet-bins
Nov 5, 2026
Merged

ddeboer merged 2 commits into
mainfrom
feat/date-facet-bins

Conversation

@ddeboer

@ddeboer ddeboer commented Nov 4, 2026 •

Copy link
Copy Markdown
Member

A date field’s facetRanges were the one place a caller still wrote a date in Unix seconds, and the GraphQL surface served those bins as RangeBucket with Float bounds. Both are now ISO 8601, and a date max – in a bin or a filter – now includes the whole period it names.

{
  name: 'dateCreated',
  kind: 'date',
  facetable: true,
  facetRanges: [
    { key: 'older', max: '1899' },
    { key: '20th century', min: '1900', max: '1999' },
    { key: 'recent', min: '2000' },
  ],
}

Why max covers the whole period

A date bound used to be the first second of the period it names, and bins were half-open while the DateRange filter is inclusive. A year-only record is stored at that first second, so a bin [1900, 2000) fed back into the filter as { min: '1900', max: '2000' } picked up every record dated just 2000: the result count no longer matched the bucket. And anyone writing “1900 to 1999” as max: '1999' silently lost 1999.

Now max: '1999' runs to 1999-12-31T23:59:59Z, max: '1999-05' to the end of May, and a date-time is just that second. Contiguous periods still partition cleanly, because one ends before the next begins. This also settles the coarse-upper-bound case #973 left out of scope, so the 1999-12-31T23:59:59Z workaround is gone from the docs. Number bins stay half-open: there the filter mismatch costs one boundary value, not a year of records.

Changes

  • Codec. lastSecondOf(stored) gives the last second of a date’s period. Deep time ends an average Gregorian year on.
  • Field model. NumericField becomes the union of NumberField (integer/number, bins as FacetRange) and DateField (bins as the new DateFacetRange, with ISO 8601 bounds inclusive at both ends). validateSearchType reports unparseable-facet-range for a bound the codec cannot read, including a leftover number in seconds.
  • Port. Buckets still echo their bounds in stored terms. storedFacetRanges gives an adapter a field’s bins as half-open Unix seconds – a date bin’s max as the first second after its period – and the Typesense adapter compiles and echoes through it. A container test buckets dates on both sides of year 1, so a negative bound reaches Typesense.
  • Filter. The Typesense compiler reads a date range’s max through lastSecondOf.
  • GraphQL. A binned date facet returns [DateRangeBucket!]!, whose min/max are ISO8601Date served at the precision they were declared in (1899, not 1899-01-01T00:00:00Z). They go straight into DateRange and select exactly the documents the bucket counts.

Follow-up to #987.

- Split NumericField into NumberField and DateField; a date's
  facetRanges are DateFacetRange, with ISO 8601 bounds
- Add storedFacetRanges, which gives an adapter the bins in Unix
  seconds; the Typesense adapter compiles and echoes through it
- Reject a date bin bound the codec cannot read
- Serve a binned date facet's buckets as DateRangeBucket, its bounds
  ISO8601Date at the precision they were declared in

BREAKING CHANGE: a date field's facetRanges take ISO 8601 strings
instead of Unix seconds ({ min: '1900', max: '2000' }), and its
binned buckets are DateRangeBucket instead of RangeBucket.
NumericField is now a union of NumberField and DateField.
- Add lastSecondOf, the last stored second of a date's period
- Compile a date range filter's max to the end of its period, so
  max: '1999' runs through 1999
- Make a date bin inclusive at both ends, like the filter; its stored
  max is the first second after its period, so bins and the filter
  that selects them count the same documents

BREAKING CHANGE: a date range filter's max, and a date bin's max, now
include the whole period they name. max: '2000' used to stop at the
first second of 2000 and now runs through its end; write max: '1999'
for a range that ends with 1999.
@ddeboer ddeboer changed the title feat(search)!: declare a date facet's bins in ISO 8601 feat(search)!: declare date facet bins in ISO 8601, with max covering its whole period Nov 5, 2026
@ddeboer
ddeboer merged commit 6ec62fa into main Nov 5, 2026
4 checks passed
@ddeboer
ddeboer deleted the feat/date-facet-bins branch November 5, 2026 09:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant