500 candidates.
One query to rank them.
A fictional home-goods marketplace retrieves hundreds of items for a shopper, then scores every one with a single Chalk bulk query: shopper history, item popularity, cross features and a ranking model, computed together. Pick a shopper, browse a category, and watch the ranking change.
Pick a shopper, then browse
Each ranking is one live Chalk bulk query over every retrieved candidate. Add a category to the shopper's session and see items move, or try Sam, who signed up five minutes ago and has no history. Tick the comparison to time the same rows as single queries. Fernhill Market, its shoppers and its catalog are made up.
Retrieve wide. Score in one pass.
Ranking needs features about the shopper, the item and the pair, for hundreds of pairs, in the time it takes a page to load. Fetching them one candidate at a time does not fit.
Retrieve
The ranking service pulls a few hundred candidate items for the shopper: some from their tastes, some popular, some to explore. Retrieval is cheap and broad.
Score in bulk
One online query carries every (shopper, item) row. Chalk resolves the shopper once, each item once, then computes the cross features and model score column-wise.
Rank and serve
The service sorts by score and returns the top items. Browsing in this session goes in with the request, so the next ranking already reflects it.
A candidate is a feature class
Model each (shopper, item) pair as a Chalk feature class that joins to the shopper and the item. Its cross features and the ranking score are expressions over both sides. Then send every candidate as a row of one bulk online query.
In this demo the ranking service is a small Python app on a Chalk scaling group, and the features are a Chalk project on the same environment. Candidate retrieval is a simple sampler standing in for a vector index.
from chalk.client.client_grpc import ChalkGRPCClient
client = ChalkGRPCClient() # or ChalkClient().query_bulk(...) over HTTP
candidate_ids = retrieve(user_id, k=500) # your ANN index or popularity sampler
# One request, one row per (shopper, item) pair.
result = client.online_query_bulk(
input={
Candidate.id: [f"{user_id}:{i}" for i in candidate_ids],
Candidate.user_id: [user_id] * len(candidate_ids),
Candidate.item_id: candidate_ids,
Candidate.session_categories: [",".join(session)] * len(candidate_ids),
},
output=[
Candidate.score,
Candidate.category_affinity_30d,
Candidate.price_misfit,
Candidate.item.title,
Candidate.item.price,
],
)
ranked = result.to_polars().sort("candidate.score", descending=True).head(10)Ranking that keeps up with the shopper
Fresh features for every candidate, computed in one request.
One round trip, not hundreds
Scoring 500 candidates is one request instead of 500. In the runs we measured, Chalk executed all 500 rows in about 75 milliseconds.
Windows at request time
Seven-day item popularity and a shopper's ninety-day spend are aggregated against the moment of the request, not last night's snapshot.
Live session context
What the shopper browsed a minute ago is a query input, combined with history in the same features. Ranking reacts on the next request.
The model is a feature
The ranking score is defined next to its inputs, so training and serving share one definition of every feature.
Expressions, not loops
Cross features and the model compile to native expressions, so cost grows with columns, not with Python calls per row.
Same features offline
The definitions that score candidates online also generate point-in-time training data offline, so the next model trains on what serving computed.
Score every candidate, every request
Talk to an engineer about serving ranking features for recommendations, search and feeds.