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, the native VECTOR type on Oracle 23ai, 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 cfg.MapVectorProperty(...), then query with store.Query<T>().NearestVectors(queryEmbedding, k: 10).
Working against Microsoft.Extensions.VectorData instead — because the rest of your AI stack expects MEVD’s VectorStore abstraction? See the VectorData Connector, which puts that face on any vector-capable provider here.
Vector Types
Section titled “Vector Types”Embedding storage
Section titled “Embedding storage”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; }}VectorDistance
Section titled “VectorDistance”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)}VectorIndexKind
Section titled “VectorIndexKind”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, Oracle 23ai — default Ivf, // pgvector ivfflat, Oracle 23ai DiskAnn, // SQL Server 2025, CosmosDB QuantizedFlat // CosmosDB}VectorResult<T>
Section titled “VectorResult<T>”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:
| Metric | Surfaced as | Direction |
|---|---|---|
| Cosine | Distance in [0, 2] |
Lower = closer |
| Euclidean | L2 distance | Lower = closer |
| DotProduct | Raw inner product | Higher = closer (negated internally on engines that don’t sort that way, so ORDER BY score ASC always means “nearest first”) |
| Hamming | Bit count | Lower = closer |
Configuration
Section titled “Configuration”Register the embedding property with MapVectorProperty<T>. The mapping captures the dimension, metric, index kind, and tuning options.
var options = new DocumentStoreOptions{ DatabaseProvider = new SqliteDatabaseProvider("Data Source=mydata.db") { EnableVectorExtension = true // load sqlite-vec on every connection }};options.ConfigureDocument<Document>(cfg => cfg.MapVectorProperty( d => d.Embedding, dimensions: 1536, metric: VectorDistance.Cosine, indexKind: VectorIndexKind.Hnsw));
var store = new DocumentStore(options);You can map multiple types — each gets its own sidecar storage and index parameters:
opts.ConfigureDocument<Memo>(cfg => cfg.MapVectorProperty(m => m.Embedding, dimensions: 1536));opts.ConfigureDocument<ProductDescription>(cfg => cfg.MapVectorProperty( 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, but ConfigureDocument is the same builder on every one of
them. Leave indexKind unset and each provider fills in its own default — DiskANN on Cosmos, HNSW elsewhere:
var options = new CosmosDbDocumentStoreOptions{ ConnectionString = "AccountEndpoint=...;AccountKey=...", DatabaseName = "mydb"};options.ConfigureDocument<Document>(cfg => cfg.MapVectorProperty(d => d.Embedding, dimensions: 1536));
var cosmos = new CosmosDbDocumentStore(options);Checking Provider Support
Section titled “Checking Provider Support”if (store.SupportsVector){ var hits = await store.NearestVectors<Document>(queryEmbedding, k: 10);}| Provider | SupportsVector |
|---|---|
| PostgreSQL (pgvector) | true |
| SQL Server 2025 | true |
Oracle 23ai (native VECTOR) |
true |
| DuckDB (vss extension) | true |
| CosmosDB | true (when vector properties are mapped) |
| MongoDB (Atlas Vector Search) | true (when vector properties are mapped) |
| SQLite (sqlite-vec) | true (with the Shiny.DocumentDb.Sqlite.VectorSupport package, or EnableVectorExtension / VectorExtensionPreloaded) |
| SQLCipher | true (same as SQLite — the VectorSupport package registers vec0 against the e_sqlcipher engine) |
| LiteDB | false |
| IndexedDB | false |
| MySQL | false |
Queries
Section titled “Queries”NearestVectors (fluent)
Section titled “NearestVectors (fluent)”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 / IDocumentSession)
Section titled “NearestVectors (low-level on IDocumentStore / IDocumentSession)”Same call without the fluent builder — useful when the filter is null or you’re not in a query chain. It lives on both the long-lived IDocumentStore and a short-lived IDocumentSession, so a unit of work can run vector search alongside its buffered writes.
var hits = await store.NearestVectors<Document>(queryEmbedding, k: 10);
// With an optional filtervar scopedHits = await store.NearestVectors<Document>( queryEmbedding, k: 10, filter: d => d.Status == "Active");Through a session it delegates to the same engine — and inside an explicit transaction it reads that transaction’s consistent snapshot, so a read-modify-write over vector results sees one stable view:
await using var session = store.OpenSession();if (session.Store.SupportsVector) // capability is a store property; reach it via session.Store{ await using var tx = await session.BeginTransaction(); // relational providers var hits = await session.NearestVectors<Document>(queryEmbedding, k: 10); // …mutate + session.Add(...); await session.SaveChanges(); all in one transaction await tx.Commit();}Filter semantics
Section titled “Filter semantics”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, Oracle, 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.postFilterMultipliercandidates so the post-filter doesn’t starve the result set (default multiplier 4).
Auto-embed on Insert
Section titled “Auto-embed on Insert”Wire Microsoft.Extensions.AI.IEmbeddingGenerator<string, Embedding<float>> so the vector is populated automatically when a text property is set. Auto-embed is a write interceptor, so it runs on every provider (relational and the document-native stores — CosmosDB, MongoDB, Redis, …), inside the write’s transaction, on Insert, BatchInsert, and Upsert, before the document is serialized.
There are two overloads:
DI overload (recommended) — resolves the generator per write from the caller’s scope (ctx.Services), so a scoped session yields the caller’s own generator (per-tenant / per-scope model selection just works). Register the generator in DI:
using Shiny.DocumentDb.Extensions.AI;
services.AddSingleton<IEmbeddingGenerator<string, Embedding<float>>>(/* e.g. AddOpenAIClient(...).AsEmbeddingGenerator() */);
services.AddDocumentStore(opts =>{ opts.ConfigureDocument<Document>(cfg => cfg.MapVectorProperty(d => d.Embedding, dimensions: 1536)); opts.AutoEmbedOnInsert<Document>( sourceSelector: d => d.Content, targetSetter: (d, vec) => d.Embedding = vec, targetGetter: d => d.Embedding); // optional — skip when vector already set});Explicit-generator overload — pass a fixed instance for the container-free new DocumentStore(options) path (or when you always want one specific model):
opts.ConfigureDocument<Document>(cfg => cfg.MapVectorProperty(d => d.Embedding, dimensions: 1536));opts.AutoEmbedOnInsert<Document>( generator, // fixed IEmbeddingGenerator instance sourceSelector: d => d.Content, targetSetter: (d, vec) => d.Embedding = vec, targetGetter: d => d.Embedding);Skip rules:
- If
sourceSelectorreturnsnullor"", it is a no-op (the vector stays at default). - If
targetGetteris provided and the existing vector is non-empty, it is a no-op — explicit writes win.
With the DI overload, if no IEmbeddingGenerator can be resolved from the write’s scope, the write throws InvalidOperationException with a clear message (register one in DI, or use the explicit overload) rather than silently writing an empty vector.
Tuning
Section titled “Tuning”VectorIndexOptions exposes the common ANN knobs plus a ProviderHints dictionary for the long tail.
opts.ConfigureDocument<Document>(cfg => cfg.MapVectorProperty( 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:
| Hint | Type | Provider | Default |
|---|---|---|---|
sqlite.postFilterMultiplier |
int | SQLite | 4 |
atlas.indexName |
string | MongoDB Atlas | vector_index_{type} |
atlas.numCandidates |
int | MongoDB Atlas | 10 * k |
Unknown keys are silently ignored per provider.
How It Works
Section titled “How It Works”PostgreSQL — pgvector sidecar table
Section titled “PostgreSQL — pgvector sidecar table”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
JOINagainst the documents table — yourWhere(...)clause is translated to SQL and runs at the planner level alongside the ANN ordering. - Per-query
SET LOCAL hnsw.ef_searchis emitted whenVectorIndexOptions.HnswEfSearchis set.
SQL Server 2025 — native VECTOR(n)
Section titled “SQL Server 2025 — native VECTOR(n)”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.
Oracle 23ai — native VECTOR(n, FLOAT32)
Section titled “Oracle 23ai — native VECTOR(n, FLOAT32)”Sidecar table with a native VECTOR(n, FLOAT32) column. VECTOR_DISTANCE(col, TO_VECTOR(@v), COSINE | EUCLIDEAN | DOT) powers the ranking — Hamming throws (it is for binary vectors). The query vector is bound as a JSON-array string and parsed by TO_VECTOR; long embeddings cross the VARCHAR2 bind limit and are sent as CLOB, which TO_VECTOR also accepts.
VectorIndexKind.Hnsw→CREATE VECTOR INDEX ... ORGANIZATION INMEMORY NEIGHBOR GRAPH DISTANCE <metric> PARAMETERS (TYPE HNSW, NEIGHBORS …, EFCONSTRUCTION …).VectorIndexKind.Ivf→... ORGANIZATION NEIGHBOR PARTITIONS DISTANCE <metric> PARAMETERS (TYPE IVF, NEIGHBOR PARTITIONS …).- Index creation is wrapped in a PL/SQL exception handler — Oracle’s vector indexes require the
vector_memory_sizepool, so where it isn’t configured the index is silently skipped and queries fall back to an exact sequential scan. FETCH APPROX FIRST k ROWS ONLYis emitted only when an index kind is requested (so Oracle can use the index);VectorIndexKind.Noneuses an exactFETCH FIRST k ROWS ONLY. Pre-filter via theJOINback to the documents table. See Oracle › Vector.
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.
DuckDB — vss extension
Section titled “DuckDB — vss extension”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.
SQLite — sqlite-vec virtual table
Section titled “SQLite — sqlite-vec virtual table”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 fork * postFilterMultipliercandidates and then filters in the JOIN.
CRUD sync
Section titled “CRUD sync”| Operation | Vector sync |
|---|---|
| Insert / Update / Upsert | Extracts the mapped vector and upserts into the sidecar |
| BatchInsert | Vector upserts run inside the same transaction so a vector write failure rolls the batch back |
| Remove | Deletes the sidecar row |
| Clear | Truncates the sidecar for the type |
IDocumentSession |
Vector queries inside a unit throw NotSupportedException — run them against the 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.


