Skip to main content

Azure AI Search with C# Tutorial: Build Smart Search (2026)

Learn Azure AI Search with C# step by step: create an index, upload documents, run full-text, semantic and vector search with the .NET SDK. Start building today.

If your application still relies on WHERE Name LIKE '%term%' for search, your users are getting a worse experience than they deserve. Azure AI Search with C# (formerly Azure Cognitive Search) gives you a fully managed search engine that handles full-text ranking, typo tolerance, faceting, semantic re-ranking and vector similarity — all accessible from .NET through the Azure.Search.Documents SDK. In this tutorial you will build a working product search from scratch: create an index, upload documents, run keyword queries, add filters and facets, and finish with hybrid vector search that powers modern RAG (Retrieval-Augmented Generation) applications.

Everything here runs on .NET 8/9 with the current stable SDK, and every code sample is complete enough to paste into a console app and run.

What Is Azure AI Search and Why Use It From C#?

Azure AI Search is a platform-as-a-service search engine. You push JSON documents into an index (think of it as a schema-defined table optimised for retrieval) and query it over REST or the SDK. Behind the scenes it runs an inverted index with BM25 ranking, an approximate-nearest-neighbour (HNSW) vector index, and optional semantic re-ranking powered by Microsoft's language models.

Why not just use SQL full-text search or Elasticsearch?

  • Zero infrastructure. No cluster to patch, scale or back up. You pick a tier and Azure runs it.
  • Relevance out of the box. Stemming, lemmatisation, synonyms and language analyzers for 50+ languages are built in.
  • First-class vector search. Store embeddings from Azure OpenAI (or any model) next to your text fields and run hybrid queries that combine both.
  • Native .NET SDK. Azure.Search.Documents is strongly typed, async-first and integrates with Azure.Identity for passwordless auth.

Prerequisites

  • An Azure subscription with an Azure AI Search resource (the Free tier is enough for this tutorial; Basic or above if you want semantic ranker).
  • .NET 8 SDK or later.
  • The service endpoint (e.g. https://my-search.search.windows.net) and an admin API key, or an Entra ID identity with the Search Index Data Contributor role.
dotnet new console -n AzureSearchDemo
cd AzureSearchDemo
dotnet add package Azure.Search.Documents
dotnet add package Azure.Identity

Step 1: Define Your Index Model in C#

The SDK can build an index schema directly from a C# class using attributes. Each attribute maps to an index capability, and choosing them deliberately matters: every IsFilterable or IsFacetable field costs storage and indexing time, so only mark what you actually query.

using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;

public class Product
{
    [SimpleField(IsKey = true, IsFilterable = true)]
    public string Id { get; set; } = default!;

    [SearchableField(IsSortable = true, AnalyzerName = LexicalAnalyzerName.Values.EnLucene)]
    public string Name { get; set; } = default!;

    [SearchableField(AnalyzerName = LexicalAnalyzerName.Values.EnLucene)]
    public string Description { get; set; } = default!;

    [SimpleField(IsFilterable = true, IsFacetable = true)]
    public string Category { get; set; } = default!;

    [SimpleField(IsFilterable = true, IsSortable = true, IsFacetable = true)]
    public double Price { get; set; }

    [SimpleField(IsFilterable = true, IsSortable = true)]
    public DateTimeOffset LastUpdated { get; set; }

    // Vector field – populated later with embeddings (1536 dims for text-embedding-3-small)
    [VectorSearchField(VectorSearchDimensions = 1536, VectorSearchProfileName = "default-vector-profile")]
    public ReadOnlyMemory<float> DescriptionVector { get; set; }
}

Why SearchableField vs SimpleField? Searchable fields are tokenised and go through the full-text analyzer, which is what you want for names and descriptions. Simple fields are stored verbatim and are ideal for IDs, prices and categories that you filter or sort on but never search with free text.

Step 2: Create the Index with the .NET SDK

SearchIndexClient manages indexes; SearchClient manages documents inside one index. Keep them as singletons — they are thread-safe and hold an HTTP connection pool.

using Azure;
using Azure.Search.Documents;
using Azure.Search.Documents.Indexes;
using Azure.Search.Documents.Indexes.Models;

var endpoint = new Uri(Environment.GetEnvironmentVariable("SEARCH_ENDPOINT")!);
var credential = new AzureKeyCredential(Environment.GetEnvironmentVariable("SEARCH_ADMIN_KEY")!);
// Preferred in production: new DefaultAzureCredential() with Azure.Identity

var indexClient = new SearchIndexClient(endpoint, credential);
const string indexName = "products";

var fieldBuilder = new FieldBuilder();
var index = new SearchIndex(indexName)
{
    Fields = fieldBuilder.Build(typeof(Product)),
    VectorSearch = new VectorSearch
    {
        Algorithms = { new HnswAlgorithmConfiguration("hnsw-config") },
        Profiles = { new VectorSearchProfile("default-vector-profile", "hnsw-config") }
    },
    SemanticSearch = new SemanticSearch
    {
        Configurations =
        {
            new SemanticConfiguration("default-semantic", new SemanticPrioritizedFields
            {
                TitleField = new SemanticField("Name"),
                ContentFields = { new SemanticField("Description") }
            })
        }
    }
};

await indexClient.CreateOrUpdateIndexAsync(index);
Console.WriteLine($"Index '{indexName}' ready.");

CreateOrUpdateIndexAsync is idempotent, which makes it safe to run at application start-up or from a deployment pipeline. Note one important limitation: you can add fields to an existing index, but you cannot change a field's type or delete it without rebuilding the index.

Step 3: Upload Documents

Documents are uploaded in batches. The SDK exposes IndexDocumentsBatch with four actions: Upload (insert or replace), Merge (partial update), MergeOrUpload and Delete.

var searchClient = indexClient.GetSearchClient(indexName);

var products = new[]
{
    new Product { Id = "1", Name = "Wireless Noise-Cancelling Headphones",
        Description = "Over-ear Bluetooth headphones with 30-hour battery and active noise cancellation.",
        Category = "Audio", Price = 249.99, LastUpdated = DateTimeOffset.UtcNow },
    new Product { Id = "2", Name = "Mechanical Keyboard",
        Description = "Compact 75% keyboard with hot-swappable switches and RGB backlighting.",
        Category = "Peripherals", Price = 129.00, LastUpdated = DateTimeOffset.UtcNow },
    new Product { Id = "3", Name = "4K Webcam",
        Description = "Ultra HD webcam with autofocus and dual noise-reducing microphones for video calls.",
        Category = "Peripherals", Price = 89.50, LastUpdated = DateTimeOffset.UtcNow }
};

var batch = IndexDocumentsBatch.Upload(products);
IndexDocumentsResult result = await searchClient.IndexDocumentsAsync(batch);

foreach (var r in result.Results.Where(r => !r.Succeeded))
    Console.WriteLine($"Failed: {r.Key} – {r.ErrorMessage}");

Pitfall: a batch can partially succeed. Always inspect result.Results rather than assuming the whole call worked. For bulk loads of thousands of documents, use SearchIndexingBufferedSender<T>, which batches, retries and throttles automatically.

Step 4: Run Full-Text Search in C#

A basic query is a single line. The SearchOptions object controls paging, selected fields, highlighting and more.

var options = new SearchOptions
{
    Size = 10,
    IncludeTotalCount = true,
    HighlightFields = { "Description" },
    HighlightPreTag = "<mark>",
    HighlightPostTag = "</mark>"
};
options.Select.Add("Id");
options.Select.Add("Name");
options.Select.Add("Price");

SearchResults<Product> results = await searchClient.SearchAsync<Product>("noise cancelling", options);

Console.WriteLine($"Total matches: {results.TotalCount}");
await foreach (SearchResult<Product> hit in results.GetResultsAsync())
{
    Console.WriteLine($"{hit.Score:F2}  {hit.Document.Name}  ${hit.Document.Price}");
    if (hit.Highlights is not null && hit.Highlights.TryGetValue("Description", out var snippets))
        Console.WriteLine("   " + string.Join(" … ", snippets));
}

Notice the query "noise cancelling" matched both "noise cancellation" and "noise-reducing" — that is the English analyzer stemming words for you. Try "noize canceling" with options.QueryType = SearchQueryType.Full and a fuzzy suffix (noize~ canceling~) to see typo tolerance in action.

Filters, Sorting and Facets

Filters use OData syntax and are applied before ranking, so they are cheap and precise. Facets return aggregate counts, perfect for the sidebar in an e-commerce UI.

var options = new SearchOptions
{
    Filter = "Category eq 'Peripherals' and Price lt 150",
    Facets = { "Category", "Price,values:50|100|200" }
};
options.OrderBy.Add("Price asc");

var results = await searchClient.SearchAsync<Product>("*", options);

foreach (var facet in results.Facets["Category"])
    Console.WriteLine($"{facet.Value}: {facet.Count}");

Security pitfall: never concatenate raw user input into Filter. Escape single quotes (value.Replace("'", "''")) or, better, use SearchFilter.Create($"Category eq {userCategory}"), which is an interpolated-string handler that escapes parameters for you.

Step 5: Add Vector and Hybrid Search for RAG Scenarios

This is where Azure AI Search with C# moves from "nice search box" to "AI application backbone". Vector search finds documents whose meaning is close to the query, even when no words overlap — "something to block out office chatter" should find the headphones. Combining vector and keyword search (hybrid) consistently beats either alone in Microsoft's published benchmarks, and adding the semantic ranker on top improves it further.

First, generate embeddings. This example uses Azure OpenAI, but any 1536-dimensional model works as long as you use the same one for indexing and querying.

// dotnet add package Azure.AI.OpenAI
using Azure.AI.OpenAI;
using OpenAI.Embeddings;

var openAi = new AzureOpenAIClient(
    new Uri(Environment.GetEnvironmentVariable("AOAI_ENDPOINT")!),
    new AzureKeyCredential(Environment.GetEnvironmentVariable("AOAI_KEY")!));

EmbeddingClient embedder = openAi.GetEmbeddingClient("text-embedding-3-small");

async Task<ReadOnlyMemory<float>> EmbedAsync(string text)
{
    OpenAIEmbedding embedding = await embedder.GenerateEmbeddingAsync(text);
    return embedding.ToFloats();
}

// Populate vectors before uploading
foreach (var p in products)
    p.DescriptionVector = await EmbedAsync($"{p.Name}. {p.Description}");

await searchClient.IndexDocumentsAsync(IndexDocumentsBatch.MergeOrUpload(products));

Now run a hybrid query: the text goes to the BM25 engine, the vector goes to HNSW, and the results are fused with Reciprocal Rank Fusion before the semantic ranker re-orders the top 50.

string userQuery = "something to block out office chatter";
ReadOnlyMemory<float> queryVector = await EmbedAsync(userQuery);

var options = new SearchOptions
{
    VectorSearch = new VectorSearchOptions
    {
        Queries =
        {
            new VectorizedQuery(queryVector)
            {
                KNearestNeighborsCount = 5,
                Fields = { "DescriptionVector" }
            }
        }
    },
    QueryType = SearchQueryType.Semantic,
    SemanticSearch = new SemanticSearchOptions
    {
        SemanticConfigurationName = "default-semantic",
        QueryCaption = new QueryCaption(QueryCaptionType.Extractive)
    },
    Size = 5
};

var results = await searchClient.SearchAsync<Product>(userQuery, options);

await foreach (var hit in results.GetResultsAsync())
{
    Console.WriteLine($"{hit.SemanticSearch?.RerankerScore:F2}  {hit.Document.Name}");
    foreach (var caption in hit.SemanticSearch?.Captions ?? [])
        Console.WriteLine("   " + caption.Text);
}

The headphones come back first with a reranker score near 3.0 even though the query shares no vocabulary with the document. Feed the top hits into a chat completion prompt and you have a working RAG pipeline: retrieval grounded in your data, generation from the LLM.

Best Practices for Azure AI Search in Production .NET Apps

  • Register clients as singletons. In ASP.NET Core, use builder.Services.AddAzureClients(b => b.AddSearchClient(endpoint, indexName, credential)) from Microsoft.Extensions.Azure. Creating a client per request exhausts sockets.
  • Use managed identity, not keys. DefaultAzureCredential works locally (via Azure CLI / Visual Studio) and in Azure (via managed identity) with zero code change. Separate query keys from admin keys if you must use keys; never ship an admin key to a browser.
  • Keep the index lean. Only mark fields filterable/facetable/sortable when a query needs it. Mark large fields IsHidden = true if they must be searchable but never returned.
  • Chunk long documents before embedding. Embedding models have token limits and retrieval quality drops for huge chunks. 300–500 tokens with ~10% overlap is a solid default; store a ParentId so you can group chunks back to the source.
  • Use indexers for data already in Azure. If your source is Blob Storage, Cosmos DB or Azure SQL, an indexer with an integrated vectorization skillset pulls, chunks and embeds data on a schedule with no custom code.
  • Handle RequestFailedException 503 with retries. The SDK retries automatically, but tune SearchClientOptions.Retry for batch indexing under load and back off when you see throttling.
  • Measure relevance. Build a small golden set of queries with expected top results and run it in CI whenever you change analyzers, scoring profiles or the embedding model.

Common Pitfalls to Avoid

  • Mismatched embedding models. Indexing with one model and querying with another yields garbage rankings without any error. Pin the model name in configuration.
  • Forgetting the index is eventually consistent. Documents are searchable a few seconds after upload. Tests that query immediately after indexing can be flaky; poll or wait.
  • Semantic ranker on the Free tier. It is not available there and the query fails. Check your tier before enabling SearchQueryType.Semantic.
  • Paging with Skip past 100,000. Deep paging is capped; use OrderBy plus a filter on the last seen key for large result sets.
  • Field name casing. The SDK serialises property names using camelCase by default via JsonSerializerOptions; FieldBuilder keeps them consistent, but hand-built SearchField definitions must match exactly.

Conclusion: Key Takeaways

Building intelligent search with Azure AI Search and C# takes far less code than most developers expect, and the payoff — relevance, typo tolerance, facets and semantic understanding — is immediately visible to users. Here is what to remember:

  • Define your schema with FieldBuilder attributes and create the index idempotently with CreateOrUpdateIndexAsync.
  • Upload in batches, always check per-document results, and use the buffered sender for bulk loads.
  • Start with full-text search plus filters and facets; it covers most use cases on its own.
  • Add vector fields and hybrid queries when you need meaning-based retrieval or a RAG pipeline, and enable the semantic ranker for the final quality boost.
  • In production, use singleton clients, managed identity, lean indexes and a relevance test suite.

Clone the snippets above into a console app, point them at a Free-tier service, and you will have a working Azure AI Search C# integration in under an hour. From there, the same index can power your website search, an internal knowledge assistant, or a full Copilot-style experience over your own data.

About csharp-coder.com
Your go-to resource for C#, .NET, and modern software development. Follow along for daily tutorials, tips, and real-world examples.

Comments

Popular posts from this blog

.NET MAUI Tutorial 2026: Build Cross-Platform Apps in C#

Learn .NET MAUI in 2026 to build iOS, Android, Windows & Mac apps from one C# codebase. Start this cross-platform tutorial with code examples today. .NET MAUI (Multi-platform App UI) is Microsoft's framework for building native iOS, Android, Windows, and macOS apps from a single C# codebase . If you've ever wanted to ship a mobile app without learning Swift, Kotlin, and Win32 separately, this .NET MAUI tutorial for 2026 is your starting point. In this guide you'll learn what .NET MAUI is, why it matters for cross-platform app development in C#, and how to build your first working app — with runnable code examples and the best practices senior engineers actually use in production. What Is .NET MAUI and Why Use It in 2026? .NET MAUI is the evolution of Xamarin.Forms, fully integrated into the modern .NET runtime. With one project and one language — C# — you target four platforms. The framework compiles to native UI controls on each device, so a button on iOS...

Angular 14 : 404 error during refresh page after deployment

In this article, We will learn how to solve 404 file or directory not found angular error in production.  Refresh browser angular 404 file or directory not found error You have built an Angular app and created a production build with ng build --prod You deploy it to a production server. Everything works fine until you refresh the page. The app throws The requested URL was not found on this server message (Status code 404 not found). It appears that angular routing not working on the production server when you refresh the page. The error appears on the following scenarios When you type the URL directly in the address bar. When you refresh the page The error appears on all the pages except the root page.   Reason for the requested URL was not found on this server error In a Multi-page web application, every time the application needs to display a page it has to send a request to the web server. You can do that by either typing the URL in the address bar, clicking on the Me...

Angular 14 CRUD Operation with Web API .Net 6.0

How to Perform CRUD Operation Using Angular 14 In this article, we will learn the angular crud (create, read, update, delete) tutorial with ASP.NET Core 6 web API. We will use the SQL Server database and responsive user interface for our Web app, we will use the Bootstrap 5. Let's start step by step. Step 1 - Create Database and Web API First we need to create Employee database in SQL Server and web API to communicate with database. so you can use my previous article CRUD operations in web API using net 6.0 to create web API step by step. As you can see, after creating all the required API and database, our API creation part is completed. Now we have to do the angular part like installing angular CLI, creating angular 14 project, command for building and running angular application...etc. Step 2 - Install Angular CLI Now we have to install angular CLI into our system. If you have already installed angular CLI into your system then skip this step.  To install angular CLI ope...