Curiosity
Rules reshaping a fused ranking — a filter drops a row, a boost lifts a personalised row to the top.

Rules & personalisation

Rules run after fusion. They are where a generic "similar items" list becomes a recommendation for this user.


Rule Purpose
r.Filter((ctx, cands) => keep) Hard filter — only the returned UIDs survive
r.BoostByRank(ctx => rankedUIDs, weight, topK, contributesAs) Soft boost — adds an RRF-style contribution to candidates in the ranked list
r.TransformFusedScore((ctx, uid, score) => newScore) Arbitrary post-processing — time decay, normalisation, score floors

Personalise: lift products the current user has bought before.

.AddRule("BoostPurchases", r => r
    .BoostByRank(
        ctx => ctx.Graph.Query()
            .StartAt(CurrentUser)
            .Out(N.Order.Type,   E.Placed)
            .Out(N.Product.Type, E.Contains)
            .AsUIDEnumerable(),
        weight: 0.3f, topK: 50,
        contributesAs: "PreviouslyPurchased"))

Constrain: keep only the requested category — conditionally.

.AddRule("CategoryFilter", r =>
{
    r.Enabled(!string.IsNullOrEmpty(input.Category));   // skip when no category asked
    r.Filter((ctx, candidates) => ctx.Graph.Query()
        .StartAt(candidates)
        .IsRelatedTo(N.Category.Type, input.Category)
        .AsUIDEnumerable());
})

Freshness: penalise stale items with a time-decay transform.

.AddRule("Recency", r => r.TransformFusedScore((ctx, uid, score) =>
{
    ctx.Graph.TryGetReadOnlyContent<Product>(uid, out var p);
    var ageDays = (DateTimeOffset.UtcNow - (p?.Released ?? default)).TotalDays;
    return score * (float)Math.Exp(-ageDays / 365.0);   // half-life ~ a year
}))

r.Enabled(false) makes any rule conditional on a request parameter — one scenario, many behaviours.

Similarity engine — rules