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 filters 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 per-signal scores (the default — shown for clarity).
    .Fuse(Fusion.Sum)
    .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 score
Fusion Combines the per-signal scores. Fusion.Sum (default) adds them; Fusion.Max lets one signal dominate; Fusion.Euclidean/Fusion.Product are soft OR/AND. Reciprocal-rank fusion is configured per signal with s.UsingReciprocalRankFusion(...)

With one signal the score is used directly; with several, they are combined by Fuse(...) (default Fusion.Sum). Add AddNegativeSignal(...) to demote candidates, and AddRule("name", r => r.Filter(...)) to drop them — rules only filter, they never change scores.

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);   // each result's ScoreInfo.Components carries a "StartAt" entry

Every result is a ScoreInfo: its Score plus a Components breakdown naming how much each signal contributed.

Similarity engine — anatomy of a scenario