Data model
Long Sequence Feature: ordered history at scale.
Store long, ordered behavior histories per entity and serve them with tail reads, time windows, and filters — the input rankers, agents, and investigation tools need without scanning a warehouse at request time.
What it is
The last N events for an entity, in order, ready to read.
A Long Sequence Feature is a per-entity, event-time-ordered log the engine understands as a first-class object. Each entity — a user, device, merchant, or agent session — owns its own sequence, and the store keeps the recent tail hot.
Reads come in three shapes: a tail read for the most recent N events, a windowed slice over a time range or count, and either with filters on type, source, or dimension. Older events stay recoverable but out of the hot path.
Because the store keeps the recent tail warm and reconstructs full history from WAL plus snapshots, a tail read stays sub-millisecond even when the entity has millions of events behind it.
Sharded across millions of sparse entity keys; each key keeps its own ordered tail warm.
Why TemporalStore
Why the temporal engine is the perfect fit for ordered history.
Recent-first history is deceptively hard to serve well. The three usual answers each break in a different place. A warehouse query ("last 200 events for this user, newest first") is a full-table scan that lands seconds later — fine for a report, fatal on a request path that has a few milliseconds to spare. A per-feature stream job keeps a materialized "last N" up to date, but you pay for it forever: a Flink or Spark topology, its state store, its checkpoints, and a new job every time a ranker wants a different N or a different filter. A Redis list is fast until you multiply it by millions of sparse entity keys — now it is memory you can't afford, with no windowed slices, no filters, and no history once it evicts.
TemporalStore collapses all three into one read because the sequence is the storage layout. Events are append-structured: a new event is a cheap ordered write to the WAL and the hot memory tier, never a read-modify-write, so there is no write amplification as a sequence grows into the millions. The most recent tail stays warm in cache, so a tail read is a bounded slice off the end — sub-millisecond regardless of how much cold history sits behind it.
The windowing and filtering run as model-aware executors at the shard, next
to the data, not in a client that has to drag every event across the network first. Asking for
"the last 50 view events in the past 24h" narrows to a bounded read and evaluates
the where predicate in place; the request path never sees the other 40,000 events.
Because each entity owns its own sequence, the model shards cleanly across
millions of sparse keys — a hot entity and a cold one cost the same per
read.
And because durability is the append log itself, history is replay-friendly: after a failover the shard reconstructs full ordered state from WAL plus snapshots, so an investigation tool can still ask "what did this entity do, in order, before the incident" with no separate archive to reconcile. One append-structured object replaces the stream job, the warehouse round trip, and the cache — new read shapes are a query, not a new pipeline.
No write amplification on append; bounded reads regardless of history depth.
In practice
Append events, then read a recent slice.
ts.append(
table="user_events",
entity="user_42",
ts_ms=event.ts_ms,
attrs={"kind": "view", "item": "sku_812"},
)
seq = ts.sequence(
table="user_events",
entity="user_42",
last=50,
since="24h",
where={"kind": "view"},
)
How to use it
An end-to-end flow, from write to a ranked request.
The full loop is four moves: append events as they happen, read the tail or a windowed slice at request time, filter to the signal a model needs, and hand the last-N to a ranker. Each read is bounded, so the shape you ask for is the cost you pay.
Write the event
Each behavior is one ordered write to the entity's sequence — WAL plus hot tier, no read-modify-write.
Grab the recent end
The last N events off the warm tail, newest first, in sub-millisecond bounded reads.
Narrow to signal
A time or count window with a where predicate evaluated at the shard, before anything crosses the wire.
Feed the model
Pass the ordered last-N straight into a ranker or an agent context pack.
ts.append(
table="user_events",
entity="user_42",
ts_ms=event.ts_ms, # event time, not wall-clock arrival
attrs={"kind": "view", "item": "sku_812", "surface": "home"},
)
Writes are keyed by entity, so each user, device, or session lands in
its own sequence — the store shards on that key across millions of entities. Always pass the
real ts_ms event time; order is by event time, so late-arriving events land in the
correct position rather than the tail.
recent = ts.sequence(
table="user_events",
entity="user_42",
last=50, # bounded: reads off the hot tail
)
A bare last=N is the cheapest read the model offers — it never
touches cold history. Keep N to what the consumer actually uses; the tail cache is
sized for recent-first access, so a last-50 read costs the same whether the entity has 500 events
or 5 million behind it.
views = ts.sequence(
table="user_events",
entity="user_42",
since="24h", # time window; or use last=N for a count window
where={"kind": "view"}, # predicate runs at the shard, not the client
limit=200, # hard cap on rows returned
)
Combine since (or last) with where to pull
just the signal a feature needs. The filter is applied in place, so a sparse event type does not
force a scan of everything in the window. limit bounds the result set; for deep
history, page with a before cursor from the oldest returned event rather than raising
limit without bound.
def rank_candidates(user_id, candidates):
history = ts.sequence(
table="user_events",
entity=user_id,
last=200,
where={"kind": "view"},
)
return ranker.score(candidates, recent_history=history) # ordered, fresh
The ranker receives ordered, request-time-fresh behavior with no feature
pipeline in between. Gotchas: keep last/limit tight on the hot path,
prefer count windows for rankers (stable input size) and time windows for investigations, and page
rather than widen a single read. After a failover the same reads work unchanged — the shard
replays ordered state from WAL plus snapshots.
When to use it
When you need recent-first history, not a warehouse scan.
Reach for a sequence whenever a request depends on what this entity did lately, in order. It replaces per-feature stream jobs and request-time warehouse queries with a single bounded, replay-friendly read.
| Use case | Sequence read | Why it fits |
|---|---|---|
| Ranking & recommendation | Last 200 interactions per user | Feeds a ranker the ordered behavior signal without a feature pipeline. |
| Agent tool history | Ordered tool calls per session | Backs Context Management with the exact action trail behind a decision. |
| Investigation trails | Windowed slice with filters | Reconstructs what happened, in order, after failover or migration. |
| Shopping & content journeys | Tail read since last session | Serves the recent path without scanning cold history. |
Related models