
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.