TTemporalStore.AI GitHub

Data model

Aggregated Feature: filtered rollups over time windows.

Compute sum, min, max, count, and grouped rollups per entity, filtered by dimension, over sliding or tumbling windows — served directly instead of rebuilt by a stream job for every new feature.

What it is

The math over a window, computed inside the store.

An Aggregated Feature turns an entity's raw events into a number — or a small set of grouped numbers — over a time window, without you shipping the events anywhere. Ask for sum, min, max, count, or rate over the last 15 minutes or 7 days, optionally filtered by dimension and grouped by another.

Each entity keeps its own sparse aggregate state, so the model scales to millions of keys without a stream job per feature. Results are fresh at request time: they reflect the latest event, not the next batch cycle.

That freshness plus request-time filtering is the edge over a nightly pipeline or a pre-computed cube — new features are a read, not a new job.

Events for one entity
checkout_eventsamount, country, method …
↓ window + filter + group, in the engine ↓
One bounded read returns
countlast 15m
sum(amount)by channel

Why TemporalStore

Why request-time rollups beat a pipeline, a cube, or hand-rolled buckets.

Every alternative to a request-time rollup trades away either freshness, coverage, or your team's time. A nightly pipeline gives you yesterday's number — useless for a fraud score or a budget cap that has to reflect the transaction happening right now, and it needs a new job every time a feature wants a different window, filter, or grouping. A pre-computed cube is fresh only for the dimensions you thought to materialize; the first analyst question that crosses an un-cubed axis falls back to a scan, and the cube's size explodes combinatorially as you add dimensions. Hand-rolled bucket code in the application — increment a Redis hash per window — works for one metric and then rots: every new window size, filter, or metric is another key schema, another TTL to tune, another race to get right.

TemporalStore computes the aggregate at the shard, at read time, over the raw events. The math — count, sum, min, max, rate — runs as a model-aware executor next to the data, so a window read reflects the latest event with no batch cycle in between. There is no pre-materialized cube to keep in sync and no separate feature to deploy: a new window, a new filter, or a new grouping is a different argument on the same call, not a new pipeline.

Freshness is free because writes are append-structured. An event is a cheap ordered append to the WAL and the hot tier — there is no read-modify-write of a running total, so there is no write amplification and no lock contention on a hot counter. The rollup is reconstructed on read from the recent window, and a multi-layer cache keeps the hot band of events warm so repeated reads over the same window stay cheap.

Because each entity keeps its own sparse aggregate state, the model scales to millions of sparse keys without paying for the ones that are idle — a merchant with a million events and one with ten cost the same per read. Grouped rollups (group_by=["channel"]) compute the whole small result set in one bounded read instead of N round trips, and after a failover the same reads work unchanged because the aggregate replays from the append log rather than a fragile external state store.

What you'd otherwise run
Nightly pipelinea day stale
Pre-computed cubefixed dimensions
Bucket codeper-metric TTL hacks
↓ replace with one read ↓
TemporalStore aggregate
Computed at the shard, at read timefresh · any window/filter/group · replay from WAL

New features are a query argument, not a new job; sparse keys cost nothing when idle.

In practice

Read a window; filter and group in the same call.

A feature window — count and sum over 15 minutes
f = ts.window(
  table="checkout_events",
  entity="user_42",
  range="15m",
  metrics=["count", "sum(amount_usd)"],
)
Filtered and grouped rollup — chargebacks by channel over 7 days
f = ts.window(
  table="chargebacks",
  entity="merchant_3",
  range="7d",
  group_by=["channel"],
  metrics=["count", "sum(amount)"],
)

How to use it

From raw events to a fresh, grouped feature vector.

The loop is: append the raw events once, then read whatever windowed statistic a decision needs — a single metric, several at once, or a grouped breakdown — computed fresh at the shard on each call. No feature is ever pre-declared; the window, filter, metrics, and grouping are all read arguments.

1 · Append

Write raw events

One ordered append per event to the entity's stream — no running total to update.

2 · Window

Read a statistic

Ask for count/sum/rate over a sliding or tumbling window, fresh to the last event.

3 · Filter + group

Slice the number

Apply a where predicate and a group_by to get a small grouped result in one read.

4 · Score

Feed the model

Assemble the metrics into a feature vector on the request path.

1 · Append raw events (the same stream powers every feature)
ts.append(
  table="checkout_events",
  entity="user_42",
  ts_ms=event.ts_ms,          # event time drives the window boundaries
  attrs={"amount_usd": 129.00, "channel": "ios", "method": "card"},
)

You write events, not aggregates. Every metric, window, and grouping below is derived from this one stream at read time, so adding a feature never means backfilling a new table — it queries events that are already there. Attribute keys (channel, method) become the dimensions you can later filter and group on.

2 · A single windowed metric — spend velocity over 15 minutes
f = ts.window(
  table="checkout_events",
  entity="user_42",
  range="15m",                # sliding window ending now
  metrics=["count", "sum(amount_usd)"],
)
# -> {"count": 3, "sum(amount_usd)": 402.5}

A range is a sliding window ending at request time, so the number always reflects the latest event. Request the metrics you need in one call; several metrics over the same window share a single bounded read rather than one read each.

3 · Filtered + grouped rollup — chargebacks by channel over 7 days
f = ts.window(
  table="chargebacks",
  entity="merchant_3",
  range="7d",
  where={"reason": "fraud"},  # predicate applied at the shard
  group_by=["channel"],       # one small result set, not N round trips
  metrics=["count", "sum(amount)"],
)
# -> {"ios": {"count": 4, "sum(amount)": 512}, "web": {"count": 1, "sum(amount)": 88}}

Filtering and grouping happen in place, so a grouped read returns the whole breakdown in one shot. Keep group_by to low-cardinality dimensions (channel, method, country) — grouping on a high-cardinality field like a raw id defeats the point; use a sequence when you need per-event rows instead of a rollup.

4 · Assemble a fresh feature vector on the request path
def risk_features(user_id):
    fast = ts.window(table="checkout_events", entity=user_id,
                     range="15m", metrics=["count", "sum(amount_usd)"])
    slow = ts.window(table="checkout_events", entity=user_id,
                     range="7d",  metrics=["count", "max(amount_usd)"])
    return {**fast, **slow}     # both fresh to the latest event, no pipeline

Mixing a short and a long window gives a scorer both burst and baseline signal, each computed fresh at read time. Gotchas: pick range to match the decision (minutes for velocity, days for baselines), prefer a handful of metrics per call over many calls, keep groupings low-cardinality, and lean on the multi-layer cache — repeated reads over the same hot window stay cheap.

When to use it

When a decision needs a fresh number over recent events.

Reach for an Aggregated Feature when you would otherwise stand up a stream job or query a warehouse just to get one windowed statistic per entity at request time.

Use caseAggregateWhy it fits
Risk & fraud featurescount / sum over minutes, filtered by dimensionFresh signal at scoring time across millions of accounts.
Campaign spend windowssum(amount) grouped by channel over a dayEnforces budgets and pacing without a spend pipeline.
Recommendation freshnesscount / rate over the recent windowReflects the latest interactions, not last night's batch.
Operational healthrate / max over a rolling windowPer-entity metrics served straight from the store.

Related models

Aggregates pair with sequences and control state.

SequencesLong Sequence FeatureRaw ordered events behind the rollups. Counters & setsControl StateCaps and distinct sets for safety. FlagshipContext ManagementAgent memory and evidence.