Skip to content
Introducing AI Conversations: Natural Language Interaction for Your Apps! Learn More

Vector / ANN Search

Vector queries let you find documents by embedding similarity — the nearest k documents to a query vector under a configurable distance metric. The library exposes a single NearestVectors API that runs on top of each provider’s native ANN engine: pgvector on PostgreSQL, the VECTOR type on SQL Server 2025, DiskANN on CosmosDB, Atlas $vectorSearch on MongoDB, the vss extension on DuckDB, and sqlite-vec on SQLite. LiteDB, IndexedDB, and MySQL throw NotSupportedException because they have no comparable engine.

The shape mirrors the spatial API: register a property with MapVectorProperty<T>(...), then query with store.Query<T>().NearestVectors(queryEmbedding, k: 10).

The vector lives on the document as a ReadOnlyMemory<float> — the same shape Microsoft.Extensions.AI.Embedding<float>.Vector returns. System.Text.Json round-trips it as a JSON array out of the box.

public class Document
{
public Guid Id { get; set; }
public string Content { get; set; } = "";
public ReadOnlyMemory<float> Embedding { get; set; }
}

The supported distance / similarity metrics.

public enum VectorDistance
{
Cosine, // distance in [0, 2]; lower = closer
Euclidean, // L2 distance; lower = closer
DotProduct, // raw inner product; higher = closer
Hamming // bit-vector distance (pgvector only)
}

The ANN index strategy the provider should provision.

public enum VectorIndexKind
{
None, // flat scan
Flat, // explicit flat index where the provider distinguishes
Hnsw, // pgvector, DuckDB vss, Atlas — default
Ivf, // pgvector ivfflat
DiskAnn, // SQL Server 2025, CosmosDB
QuantizedFlat // CosmosDB
}

Wraps a document with its computed similarity score.

public class VectorResult<T> where T : class
{
public required T Document { get; init; }
public float Score { get; init; }
}

Score semantics are stable across providers:

MetricSurfaced asDirection
CosineDistance in [0, 2]Lower = closer
EuclideanL2 distanceLower = closer
DotProductRaw inner productHigher = closer (negated internally on engines that don’t sort that way, so ORDER BY score ASC always means “nearest first”)
HammingBit countLower = closer

Register the embedding property with MapVectorProperty<T>. The mapping captures the dimension, metric, index kind, and tuning options.

var store = new DocumentStore(new DocumentStoreOptions
{
DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db")
{
EnableVectorExtension = true // load sqlite-vec on every connection
}
}.MapVectorProperty<Document>(
d => d.Embedding,
dimensions: 1536,
metric: VectorDistance.Cosine,
indexKind: VectorIndexKind.Hnsw));

You can map multiple types — each gets its own sidecar storage and index parameters:

opts.MapVectorProperty<Memo>(m => m.Embedding, dimensions: 1536)
.MapVectorProperty<ProductDescription>(p => p.Embedding,
dimensions: 384,
metric: VectorDistance.DotProduct);

Provider-specific options (CosmosDbDocumentStoreOptions, MongoDbDocumentStoreOptions)

Section titled “Provider-specific options (CosmosDbDocumentStoreOptions, MongoDbDocumentStoreOptions)”

Cosmos and Mongo carry their own option classes (no shared base with DocumentStoreOptions). Each exposes the same MapVectorProperty<T> overloads with the provider-appropriate default for indexKind:

var cosmos = new CosmosDbDocumentStore(new CosmosDbDocumentStoreOptions
{
ConnectionString = "AccountEndpoint=...;AccountKey=...",
DatabaseName = "mydb"
}.MapVectorProperty<Document>(d => d.Embedding,
dimensions: 1536,
indexKind: VectorIndexKind.DiskAnn));
if (store.SupportsVector)
{
var hits = await store.NearestVectors<Document>(queryEmbedding, k: 10);
}
ProviderSupportsVector
PostgreSQL (pgvector)true
SQL Server 2025true
DuckDB (vss extension)true
CosmosDBtrue (when vector properties are mapped)
MongoDB (Atlas Vector Search)true (when vector properties are mapped)
SQLite (sqlite-vec)true (when EnableVectorExtension = true)
SQLCiphertrue (inherits SQLite vec0 support)
LiteDBfalse
IndexedDBfalse
MySQLfalse

The preferred entry point. Where(...) predicates pre-filter where the provider supports it; OrderBy / Paginate / GroupBy are ignored (use k to bound the result count).

var hits = await store.Query<Document>()
.Where(d => d.Tenant == tenantId)
.NearestVectors(queryEmbedding, k: 10);
foreach (var hit in hits)
Console.WriteLine($"{hit.Score:F4} {hit.Document.Content}");

NearestVectors (low-level on IDocumentStore)

Section titled “NearestVectors (low-level on IDocumentStore)”

Same call without the fluent builder — useful when the filter is null or you’re not in a query chain.

var hits = await store.NearestVectors<Document>(queryEmbedding, k: 10);
// With an optional filter
var scopedHits = await store.NearestVectors<Document>(
queryEmbedding, k: 10,
filter: d => d.Status == "Active");

The contract is “matches the Where predicates AND is in the top-k nearest”. The order of operations is provider-dependent and documented per backend below.

  • CosmosDB, pgvector, SQL Server, Atlas, DuckDB — pre-filter inside the ANN search. Result count == k unless fewer documents match.
  • SQLite (sqlite-vec) — ANN candidates are produced first, then the predicate is applied. The library pulls k * sqlite.postFilterMultiplier candidates so the post-filter doesn’t starve the result set (default multiplier 4).

Wire Microsoft.Extensions.AI.IEmbeddingGenerator<string, Embedding<float>> so the vector is populated automatically when a text property is set. The hook fires inside Insert, BatchInsert, and Upsert before the document is serialized.

using Shiny.DocumentDb.Extensions.AI;
var generator = /* resolved from DI, e.g. AddOpenAIClient(...).AddEmbeddingGenerator(...) */;
opts.MapVectorProperty<Document>(d => d.Embedding, dimensions: 1536)
.AutoEmbedOnInsert<Document>(
generator,
sourceSelector: d => d.Content,
targetSetter: (d, vec) => d.Embedding = vec,
targetGetter: d => d.Embedding); // optional — skip when vector already set
await store.Insert(new Document { Content = "hello world" });
// document.Embedding is now populated.

Skip rules:

  • If sourceSelector returns null or "", the hook is a no-op (the vector stays at default).
  • If targetGetter is provided and the existing vector is non-empty, the hook is a no-op — explicit writes win.

If IEmbeddingGenerator isn’t available at runtime (the hook captured null), the first Insert throws InvalidOperationException with a clear message instead of silently writing an empty vector.

VectorIndexOptions exposes the common ANN knobs plus a ProviderHints dictionary for the long tail.

opts.MapVectorProperty<Document>(
d => d.Embedding,
dimensions: 1536,
metric: VectorDistance.Cosine,
indexKind: VectorIndexKind.Hnsw,
configureIndex: i =>
{
i.HnswM = 16;
i.HnswEfConstruction = 64;
i.HnswEfSearch = 40;
i.IvfLists = 100;
i.ProviderHints["sqlite.postFilterMultiplier"] = 8;
i.ProviderHints["atlas.indexName"] = "my-vec-index";
i.ProviderHints["atlas.numCandidates"] = 200;
});

Recognized hints:

HintTypeProviderDefault
sqlite.postFilterMultiplierintSQLite4
atlas.indexNamestringMongoDB Atlasvector_index_{type}
atlas.numCandidatesintMongoDB Atlas10 * k

Unknown keys are silently ignored per provider.

CREATE EXTENSION IF NOT EXISTS vector runs idempotently on every connection. Each (documents-table, document-type) pair gets its own sidecar table with a vector(n) column and an HNSW or IVF index when configured.

  • Operators used per metric: <=> (cosine), <-> (L2), <#> (negative inner product), <+> (Hamming).
  • Pre-filter via JOIN against the documents table — your Where(...) clause is translated to SQL and runs at the planner level alongside the ANN ordering.
  • Per-query SET LOCAL hnsw.ef_search is emitted when VectorIndexOptions.HnswEfSearch is set.

Sidecar table with a VECTOR(n) column. VECTOR_DISTANCE('cosine' | 'euclidean' | 'dot', col, @v) powers the ranking, and CREATE VECTOR INDEX ... WITH (METRIC = ..., TYPE = DISKANN) is attempted under a TRY/CATCH so older SQL Server versions degrade to sequential scan rather than failing table init. Hamming throws.

CosmosDB — embedding policy + VectorDistance()

Section titled “CosmosDB — embedding policy + VectorDistance()”

The container’s VectorEmbeddingPolicy and IndexingPolicy.VectorIndexes are configured on first touch from the mapped properties. The query is a standard Cosmos SQL SELECT TOP @k ... ORDER BY VectorDistance(...) with the score returned as a projection. Pre-filter via your Where(...) predicate translated by the existing Cosmos expression visitor.

MongoDB — $vectorSearch aggregation (Atlas only)

Section titled “MongoDB — $vectorSearch aggregation (Atlas only)”

The query is built as a two-stage aggregation: $vectorSearch with path, queryVector, numCandidates, limit, and a filter clause for the type-name match, then $project to surface the document body and the vectorSearchScore meta.

Atlas requires a pre-existing vector search index. Use atlas.indexName to point at an existing index (default convention is vector_index_{type}). On-prem MongoDB throws NotSupportedException with a clear message — $vectorSearch is an Atlas feature.

The vss extension is auto-loaded on every connection alongside json. The sidecar table stores FLOAT[N] arrays, optionally indexed with HNSW. Distance functions: array_cosine_distance, array_distance (L2), array_inner_product (negated for ORDER BY ASC semantics). HNSW persistence on file-backed DBs uses the hnsw_enable_experimental_persistence flag.

Two sidecar tables per type — a vec0 virtual table and an integer-rowid map (vec0 indexes only integer rowids, mirroring the R*Tree spatial pattern). The sqlite-vec extension is loaded on every connection via SqliteConnection.LoadExtension(VectorExtensionPath).

  • Extension loading is opt-in via SqliteDatabaseProvider.EnableVectorExtension = true. The user must ship the native binary; the loader searches the standard OS paths and the app directory.
  • vec0 has no HNSW — searches are flat-scan. When a Where(...) filter is in play, the library asks vec0 for k * postFilterMultiplier candidates and then filters in the JOIN.
OperationVector sync
Insert / Update / UpsertExtracts the mapped vector and upserts into the sidecar
BatchInsertVector upserts run inside the same transaction so a vector write failure rolls the batch back
RemoveDeletes the sidecar row
ClearTruncates the sidecar for the type
RunInTransaction (v6)Vector queries inside a transaction throw NotSupportedException — run them against the outer store

A vector field that’s left at default (zero-length ReadOnlyMemory<float>) is skipped on the write path — the document is stored without an embedding. Dimension mismatches throw ArgumentException with the document Id and expected/actual dimension so the failure is easy to diagnose.