Live demo · Recommendations

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.

20 rankings a minute · comparison runs limited to 4 a minuteOpen in new tab

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.

Getting started

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.