Azure AI Search vector search .NET code is easier to reason about when you see it as a precise contract: a query embedding is sent to a named vector field in an existing index, and the service returns the nearest indexed documents. That sounds small, but the boundary has several failure modes. The query vector uses a different embedding space than the stored vectors. The field name is wrong. The result model omits the metadata that explains what was returned.
This article focuses on direct, first-stage vector queries using the stable Azure.Search.Documents client library. It assumes that an index already exists and that your application can produce a query vector. It does not cover service provisioning, connector abstractions, query fusion, or second-stage ranking. Those are separate concerns. The point here is to build a clear mental model for the request that turns a ReadOnlyMemory<float> into useful, typed search results.
Azure AI Search vector search .NET starts with two compatible representations
A vector search request has a document side and a query side. During indexing, an application stores a floating-point vector with each searchable chunk. At query time, it uses the same embedding contract to turn the user's question into another vector. Azure AI Search compares the query vector with the vectors in the requested field and returns nearest neighbors.
The important word is compatible. A vector is not a universal representation of meaning. Its dimensions and values belong to the model that generated it. The Azure AI Search vector search overview describes vector search as indexing and querying numeric embeddings for similarity matching. In practice, a query vector must have the same number of dimensions as the index field it targets; a 1,536-dimensional field cannot accept a 3,072-dimensional query vector.
That contract is more than an implementation detail. Treat the embedding model identity and vector dimension as part of the index schema. When either changes, the existing document vectors and new query vectors are no longer interchangeable. Re-index the affected corpus deliberately rather than silently sending mismatched values into a query path.
This is also why a direct SDK view is useful even when another framework sits around it. My earlier Semantic Kernel vector store article covers the framework-level abstraction. For framework-specific embedding-generation background, see the Semantic Kernel embeddings guide. Direct-client work is about the concrete index field, request object, and result shape underneath that abstraction.
Use the current stable package surface
As rechecked on 2026-08-10, NuGet lists Azure.Search.Documents 12.0.0 as the latest stable package and 12.1.0-beta.1 as a prerelease, while Azure AI Search lists SDK version 12 as active. Use the stable package for implementation guidance here.
<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.17.1" />
<PackageReference Include="Azure.Search.Documents" Version="12.0.0" />
</ItemGroup>
The Azure AI Search API version guidance also documents that 2023-07-01-preview was deprecated and is no longer supported. That distinction matters when copying older snippets. The quickstart's stable Azure Search API usage uses SearchClient, SearchOptions, VectorSearchOptions, and VectorizedQuery; its project file pins Azure.Search.Documents 12.0.0 but also includes an unrelated Azure.AI.OpenAI beta dependency that this article neither uses nor recommends.
The package reference alone is not the contract, though. An index needs a vector field, a vector-search profile, and a profile algorithm configuration before a direct query can name the field. The following example defines only those pieces of an index contract. It deliberately creates no service resources and uploads no content.
using Azure.Search.Documents.Indexes.Models;
public static class ChunkIndexDefinition
{
public static SearchIndex Create()
{
var vectorSearch = new VectorSearch();
vectorSearch.Algorithms.Add(
new HnswAlgorithmConfiguration("chunk-hnsw"));
vectorSearch.Profiles.Add(
new VectorSearchProfile("chunk-vector-profile", "chunk-hnsw"));
return new SearchIndex("knowledge-chunks")
{
Fields =
{
new SimpleField("Id", SearchFieldDataType.String)
{
IsKey = true,
IsFilterable = true
},
new SearchableField("Content"),
new VectorSearchField(
"ContentVector",
vectorSearchDimensions: 1536,
vectorSearchProfileName: "chunk-vector-profile"),
new SimpleField("SourceUri", SearchFieldDataType.String),
new SimpleField("TenantId", SearchFieldDataType.String)
{
IsFilterable = true
}
},
VectorSearch = vectorSearch
};
}
}
ContentVector is a field name, not a C# property convention. The query must use the exact field name stored in the index definition. The 1536 value is likewise illustrative of an embedding output size, not a value to copy by habit. Choose the configured dimension that matches the model used for both documents and queries.
This separation is useful in a RAG application too. An embedding component owns turning text into a vector. The search component owns the direct query request. If you are looking for the application-level flow around those pieces, the existing RAG with Semantic Kernel guide provides that framework-specific companion without changing the direct query model described here.
Make the vector field and query vector agree
A configured vector field does not generate embeddings; your application supplies the vector at index time and at query time, or uses a separately configured indexing path, as the vector-query guidance explains. For a direct query, VectorizedQuery receives the already-generated vector as ReadOnlyMemory<float>.
The next example makes the compatibility boundary explicit before it creates a search request. It also keeps the index field name in one place. This is a small practice with a large payoff: a model or schema migration has an obvious seam to change and review.
using Azure.Search.Documents.Models;
public sealed record VectorFieldContract(string Name, int Dimensions)
{
public VectorizedQuery CreateQuery(ReadOnlyMemory<float> vector, int topK)
{
if (vector.Length != Dimensions)
{
throw new ArgumentException(
$"Expected {Dimensions} dimensions, but received {vector.Length}.",
nameof(vector));
}
return new VectorizedQuery(vector)
{
KNearestNeighborsCount = topK,
Fields = { Name }
};
}
}
VectorizedQuery represents a raw vector supplied by the caller, KNearestNeighborsCount sets the requested top hits, and Fields identifies the vector field or fields to search. These are direct, stable Azure.Search.Documents 12.0.0 APIs, as shown by the official C# vector-search quickstart.
Notice that this code does not pretend to know how the vector was made. A query embedding can come from a provider-specific client or an internal embedding boundary. What matters to the direct query is that the vector has the correct dimension and represents text in the same embedding space as the stored chunks; Azure recommends using the same embedding model used for source documents. Matching only the dimension is necessary, but it is not proof of semantic compatibility. Record the embedding model and revision with your indexed corpus so that an accidental model swap is detectable.
Send a direct vector request with SearchClient
SearchClient performs search operations against the index named when the client is created, and SearchAsync<T> maps selected index fields onto a .NET type. Keeping the result shape small prevents the retrieval layer from quietly becoming a second document store in memory.
using Azure;
using Azure.Identity;
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
public sealed record SearchChunk(
string Id,
string Content,
string SourceUri,
string TenantId);
public sealed record RetrievedChunk(SearchChunk Document, double? Score);
public static class ChunkSearch
{
public static async Task<IReadOnlyList<RetrievedChunk>> SearchAsync(
Uri endpoint,
string indexName,
ReadOnlyMemory<float> queryVector,
CancellationToken cancellationToken)
{
var client = new SearchClient(
endpoint,
indexName,
new DefaultAzureCredential());
var contract = new VectorFieldContract("ContentVector", 1536);
var response = await client.SearchAsync<SearchChunk>(
new SearchOptions
{
VectorSearch = new VectorSearchOptions
{
Queries = { contract.CreateQuery(queryVector, topK: 5) }
},
Select = { "Id", "Content", "SourceUri", "TenantId" }
},
cancellationToken);
var chunks = new List<RetrievedChunk>();
await foreach (SearchResult<SearchChunk> result in response.Value.GetResultsAsync())
{
chunks.Add(new RetrievedChunk(result.Document, result.Score));
}
return chunks;
}
}
The call uses the overload that takes SearchOptions, so it is a vector-only request. VectorSearchOptions.Queries contains the VectorizedQuery, and Select limits the returned retrievable fields to the values the application needs. The SDK's SearchClient.SearchAsync API reference documents both the generic mapping and asynchronous result enumeration through GetResultsAsync().
SearchResult<T>.Score is a nullable double relevance score relative to the documents returned by the query. For vector-only results, its value depends on the configured similarity metric; Azure transforms the cosine score for ranking rather than returning raw cosine similarity. Treat that score as information from this query, not as a portable measure of truth or a confidence percentage. It can help inspect why one returned chunk precedes another, but an application still needs evaluation data to decide whether the returned chunks answer its users' questions. For a complete application example and retrieval-oriented context, see Build a Document Q&A App with RAG and Semantic Kernel.
Filter metadata without treating it as a vector
A vector field is not filterable, so filters belong on nonvector metadata fields. Metadata fields are where you express boundaries such as source category, lifecycle state, or an authorization scope derived by server policy.
Here is the same direct query model with a metadata filter. The example uses a fixed, server-derived tenant value to keep the OData expression focused. In an application, construct and validate this boundary from authenticated server-side policy. Do not accept an arbitrary filter from a browser and treat it as authorization.
using Azure.Search.Documents;
using Azure.Search.Documents.Models;
public static class ScopedChunkSearch
{
public static Task<Response<SearchResults<SearchChunk>>> SearchAsync(
SearchClient client,
ReadOnlyMemory<float> queryVector,
CancellationToken cancellationToken)
{
var contract = new VectorFieldContract("ContentVector", 1536);
return client.SearchAsync<SearchChunk>(
new SearchOptions
{
VectorSearch = new VectorSearchOptions
{
Queries = { contract.CreateQuery(queryVector, topK: 5) }
},
Filter = "TenantId eq 'contoso'",
Select = { "Id", "Content", "SourceUri", "TenantId" }
},
cancellationToken);
}
}
The lesson is not the literal tenant name. It is the schema split: ContentVector holds numeric embeddings, while TenantId is a filterable metadata field. That gives a direct vector query a place to express an application boundary without asking the vector itself to carry query semantics it does not support. The Semantic Kernel semantic search engine article is useful if you want to compare that higher-level application style with this focused, direct-client request.
Read scores and metadata as retrieval evidence
A useful vector-search result is more than chunk text. Preserve the chunk identifier, source location, and any metadata needed to explain why the application used it. That makes a retrieval path debuggable: you can inspect the question vector's contract, the field it searched, the selected result set, and the source each result came from.
The Azure quickstart's vector query returns documents and scores, and KNearestNeighborsCount controls the requested top results. The number is a retrieval choice, not a universal quality setting. A small value may omit needed context; a large value can return more material than a downstream answer step should receive. Start by inspecting result sets for representative questions, then evaluate them against known relevant chunks.
For Azure AI Search vector search .NET, distinguish three practical failure classes:
- A dimension mismatch is a contract failure between the query vector and the field.
- A compatible but unrelated embedding model is a data migration failure that dimensions alone cannot reveal.
- A plausible result set that lacks the needed source is a retrieval-quality failure that requires evaluated queries and corpus review.
Those categories lead to different fixes. The first needs schema or embedding correction. The second needs a controlled re-index. The third needs retrieval evaluation, content inspection, or a different query strategy. Conflating them under “vector search is inaccurate” slows down every investigation.
Test the contract before production traffic finds the mismatch
A useful early validation is deterministic and does not need a live query. Given a known index contract, test that the query vector has the declared dimension and that the request names only the intended vector field. The VectorFieldContract example makes that test inexpensive because it fails before a network call. It complements integration tests against a reviewed index and corpus. Keep a second test around the selected field names so a model type change does not quietly remove Id or SourceUri from your retrieval evidence.
Then use a small, reviewed set of corpus questions to inspect actual result sets. For each question, record the expected source or chunk identifiers, the embedding model revision, the index version, and the requested top-K value. The goal is not to declare one score universally good. It is to make a future change explainable. When a source disappears from results, you can determine whether the corpus, embedding contract, field, or query behavior changed.
Metadata filters deserve the same treatment. Test a positive case that returns an eligible chunk and a negative case that excludes one outside the server-derived scope. A filter is part of the retrieval request, so it belongs in test fixtures and operational review rather than in a UI-only convenience layer. This preserves the distinction between a query that failed to find evidence and a request that correctly withheld evidence.
Frequently asked questions
What package should I use for Azure AI Search vector search .NET?
As rechecked on 2026-08-10, NuGet lists Azure.Search.Documents 12.0.0 as stable and 12.1.0-beta.1 as prerelease, while Azure AI Search lists SDK version 12 as active. It supplies SearchClient, SearchOptions, VectorSearchOptions, and VectorizedQuery. Keep preview packages out of a stable implementation unless you have deliberately accepted their API lifecycle.
Does a vector field create embeddings for me?
No. In a direct vector query, your application provides the query vector unless you use separately configured integrated vectorization. The field defines where matching document vectors live and what vector-search profile they use. The query vector must match the field's configured dimensionality and embedding contract.
Why does Azure AI Search vector search .NET need a field name?
An index can contain more than one vector field. VectorizedQuery.Fields identifies the Collection(Edm.Single) vector field or fields to search. That explicit name prevents the SDK from guessing which representation of a document should be compared with the query.
Is the returned score a confidence percentage?
No. SearchResult<T>.Score is a nullable relevance score relative to returned documents, and Azure's cosine vector score is transformed for ranking rather than being a confidence percentage. It is useful for inspecting order and behavior, but it is not a calibrated statement that a result is correct, complete, or safe to use without application-level evaluation.
Can I filter a vector field directly?
No. Put filterable metadata, such as a server-derived scope or source state, in a separate nonvector field. Apply a filter to that metadata while the vector query targets its named vector field.
When should I change the vector dimensions?
Change dimensions only when you intentionally change the embedding contract and are ready to re-index the affected vectors. A new query vector cannot become compatible with an old corpus merely because both values are stored as floating-point arrays.
Keep the query model small and explicit
The durable Azure AI Search vector search .NET model is straightforward: configure a vector field with the correct dimensions, generate a compatible query vector, create VectorizedQuery, name the field, request a bounded number of neighbors, and keep the metadata that makes results explainable. The SDK call is short. The contract around it deserves the careful part of the design.
Once that direct request is clear, it becomes easier to diagnose the real question when retrieval looks wrong: did the index field, embedding model, query vector, metadata boundary, or corpus create the result set? That is a much better starting point than treating every retrieval problem as a mystery inside a single API call.

