A four-stage pipeline diagram illustrating Signals, Fusion, Rules, and Results with icons and connections.

The recommendation engine

A single lookup answers "what is like this?". For "what should we recommend?", IQuery.ToSimilarity(...) combines multiple signals into one ranking with fusion, then clips or reweights it with rules.


var result = await Graph.Query()
    .StartAt(seedUID)                          // the subject signals see in ctx.Subjects
    .ToSimilarity(o => o.MaxCandidates(200))

    // 1. Text similarity over product names (embedding signal).
    .AddSignal("SimilarName", s => s
        .Weight(1.0f)
        .FromAsync(async ctx =>
            (await ctx.Graph.Query().StartAtSimilarTextAsync(
                seedName, count: 100, nodeTypes: new[] { N.Product.Type },
                indexUID: Indexes.Product.SentenceEmbeddingsIndex_Name_ArcticXS, applyCutoff: false))
            .Except(ctx.Subjects)))            // returns the IQuery — its similarity scores are kept

    // 2. Same manufacturer (graph traversal signal).
    .AddSignal("SameManufacturer", s => s
        .Weight(0.7f)
        .From(ctx => ctx.Graph.Query().StartAt(ctx.Subjects)
            .Out(N.Manufacturer.Type, E.ManufacturedBy)
            .Out(N.Product.Type, E.Manufactures)
            .Except(ctx.Graph.Query().StartAt(ctx.Subjects))))

    // Combine the rankings.
    .Fuse(f => f.UsingReciprocalRankFusion())
    .ExecuteAsync(ct);

Piece What it does
Signal A candidate source returning an IQuery. Text, graph, external — as many as you need. If the query carries scores (e.g. StartAtSimilarTextAsync), the engine uses them; otherwise it ranks by position
Weight(...) Scales a signal's contribution to the fused rank
Fusion Merges the rankings. ReciprocalRankFusion (default) is robust across different score scales and rewards consensus; MaxScore lets one signal dominate

With one signal, the scenario uses its scores directly. With two or more, you must call Fuse(...) or it falls back to MaxScore (rarely what you want).

You can also start the scenario straight from a similarity search — the initial scores become the first signal:

var result = await (await Graph.Query()
        .StartAtSimilarTextAsync(seedName, count: 100, nodeTypes: new[] { N.Product.Type }))
    .ToSimilarity()
    .ExecuteAsync(ct);   // result.Signals["StartAt"] holds the seeding similarity scores

Similarity engine — anatomy of a scenario