Similarity Search with IQuery
IQuery exposes three entry points for vector search. They all read from the same embedding indexes (Sentence, PageSpace, Raw), and they all return a normal IQuery you can continue chaining — filter, project, emit.
| Method | Starts from | Returns |
|---|---|---|
IQuery.Similar(IndexTypes? indexType, IndexUID indexUID, int count) |
UIDs already in the chain | Neighbors of each current UID, ranked. |
IQuery.StartAtSimilarTextAsync(string text, int count, string[] nodeTypes, IndexUID indexUID, bool applyCutoff) |
Text | Nearest nodes to the encoded text vector. |
IQuery.ToSimilarity(...) |
UIDs | A multi-signal scenario — see Similarity Engine. |
Everything on this page assumes you're inside a custom endpoint, a code index, an AI tool, or the shell — i.e. anywhere Graph and Q() are in scope.
Similar — neighbors of the current set
Similar takes the UIDs currently in the chain and replaces them with their nearest neighbors:
// Find 20 products similar to a seed.
return Q().StartAt(productUID)
.Similar(count: 20)
.EmitWithScores();
With no indexType or indexUID, the workspace picks every ISimilarityIndex registered for the seed's node type and merges their results. That's convenient but rarely what you want once you have more than one index per type.
Narrowing to a specific index
There are two ways to pin the lookup to one index — by type or by UID:
// 1. By index type — match any PageSpace embeddings index for this node type.
return Q().StartAt(customerUID)
.Similar(IndexTypes.PageSpaceEmbeddingsIndex, count: 20)
.EmitWithScores();
// 2. By explicit UID — pinpoint exactly one index instance.
var indexUID = Graph.Indexes
.OfType<SentenceEmbeddingsIndex>(N.Product.Type)
.First(i => i.FieldName == N.Product.Description)
.UID;
return Q().StartAt(productUID)
.Similar(indexUID: indexUID, count: 20)
.EmitWithScores();
Pass an indexUID whenever multiple indexes for the same node type exist (e.g. you index both Description and Name, or you have a RawEmbeddingsIndex from an external provider alongside the built-in SentenceEmbeddingsIndex).
Chaining after Similar
Similar emits scored UIDs and rebuilds the chain. You can keep filtering and traversing as usual:
return Q().StartAt(productUID)
.Similar(indexUID: productNameIndex, count: 100)
.ExceptType(N.DiscontinuedProduct.Type)
.Where(N.Product.Status, "Available")
.Take(20)
.EmitWithScores();
EmitWithScores() is the only emitter that preserves the similarity score; the rest emit results without it.
StartAtSimilarTextAsync — encode and search
Encodes a piece of text and returns its nearest neighbors. This is the building block for "semantic search" UIs that don't need full BM25 + hybrid fusion.
var query = await Q().StartAtSimilarTextAsync(
text: "battery drains overnight",
count: 20,
nodeTypes: new[] { N.SupportCase.Type });
return query.EmitWithScores();
The lookup runs against every ITextSimilarityIndex whose NodeType matches the filter and that has AISearchEnabled = true. The defaults are tuned for the search UI; when calling from code:
| Parameter | Meaning |
|---|---|
text |
The query text. Empty/whitespace returns an empty query. |
count |
Max neighbors per index. With three matching indexes you can get up to 3 × count candidates merged. |
nodeTypes |
Restrict to indexes whose NodeType is in this set. |
indexUID |
Bypass AISearchEnabled and target exactly one index. Set this when you want a specific encoder regardless of admin config. |
applyCutoff |
If true, hits below the index's InjectResultCutoff are dropped before being returned. |
Targeting a specific index
var arctic = Graph.Indexes
.OfType<SentenceEmbeddingsIndex>(N.SupportCase.Type)
.First(i => i.SentenceEncoderModel == SentenceEncoderModel.ArcticXS);
var query = await Q().StartAtSimilarTextAsync(
text: input.Query,
count: 50,
nodeTypes: new[] { N.SupportCase.Type },
indexUID: arctic.UID,
applyCutoff: true);
return query.Take(20).EmitWithScores();
This is also the right call to use from a similarity engine signal: wrap it in s.FromAsync(...) to feed text-encoded candidates into a multi-signal scenario.
A complete endpoint — /similar-products
A typical endpoint takes a seed UID, narrows to a specific index, and returns the top-K with scores. The example below uses a sample Product dataset modelled after the workspace's similarity-engine tests:
| Node | Key fields | Edges |
|---|---|---|
Product |
Name, Description, SKU, Status |
→ Manufacturer (ManufacturedBy), → Tag (HasTag), → Category (InCategory) |
Manufacturer |
Name |
← Product (Manufactures) |
Category |
Name |
← Product (InCategory) |
Tag |
Name |
← Product (HasTag) |
1. Register a sentence-embeddings index on Product.Name
A migration or one-off shell call sets up the index — see Sentence Embeddings for the full options reference.
await Graph.Indexes.AddSentenceEmbeddingsIndexAsync(
nodeType: N.Product.Type,
fieldName: N.Product.Name,
model: SentenceEncoderModel.ArcticXS);
2. The endpoint
public record SimilarRequest(string ProductId, int TopK = 10);
public record ScoredProductDto(
string Id,
string Name,
string Manufacturer,
double Score);
var input = Body.FromJson<SimilarRequest>();
if (input is null || string.IsNullOrWhiteSpace(input.ProductId))
return BadRequest("`productId` is required.");
if (!Graph.TryGetReadOnlyContent<Product>(N.Product.Type, input.ProductId, out var seed))
return NotFound($"Product '{input.ProductId}' not found.");
// Pick the specific index we want to consume.
var nameIndex = Graph.Indexes
.OfType<SentenceEmbeddingsIndex>(N.Product.Type)
.FirstOrDefault(i => i.FieldName == N.Product.Name);
if (nameIndex is null)
return Problem("Sentence embeddings index on Product.Name is not registered.");
// Run the similarity lookup and shape the response.
var hits = Q().StartAt(seed.UID)
.Similar(indexUID: nameIndex.UID, count: input.TopK + 1)
.AsScoredUIDEnumerable()
.Where(s => s.UID.UID != seed.UID) // drop the seed itself
.Take(input.TopK)
.Select(scored =>
{
Graph.TryGetReadOnlyContent<Product>(scored.UID.UID, out var p);
var manufacturer = Q().StartAt(scored.UID.UID)
.Out(N.Manufacturer.Type, E.ManufacturedBy)
.AsEnumerable()
.FirstOrDefault();
return new ScoredProductDto(
Id: p?.Key ?? scored.UID.UID.ToString(),
Name: p?.Name ?? "",
Manufacturer: manufacturer?.GetString(N.Manufacturer.Name) ?? "",
Score: scored.Score);
})
.ToList();
return Ok(new { source = input.ProductId, hits }.ToJson(), "application/json");
A request like POST /endpoints/similar-products {"productId": "P-2199", "topK": 5} returns:
{
"source": "P-2199",
"hits": [
{ "id": "P-2207", "name": "Wireless Mouse Pro", "manufacturer": "Logitech", "score": 0.871 },
{ "id": "P-2153", "name": "Ergonomic Mouse", "manufacturer": "Logitech", "score": 0.812 },
{ "id": "P-2244", "name": "Wireless Trackball", "manufacturer": "Kensington","score": 0.768 },
{ "id": "P-2031", "name": "Bluetooth Mouse Lite", "manufacturer": "Microsoft", "score": 0.752 },
{ "id": "P-2412", "name": "Gaming Mouse RGB", "manufacturer": "Razer", "score": 0.741 }
]
}
The next page shows how to take the same scored-neighbors output for many seeds and turn it into a clusterable WeightedGraph.
See also
- Sentence Embeddings, Graph Embeddings, Raw Embeddings — the indexes
Similartalks to. - Similarity Engine — when one signal isn't enough.
- Searching from Endpoints — full-text + hybrid retrieval.
- Graph Query Language — the full
IQueryreference.