Quickstart: Create a search index using the legacy Microsoft.Azure.Search v10 client library

This article is the C# quickstart for the legacy Microsoft.Azure.Search (version 10) client library, now superseded by the Azure.Search.Documents (version 11) client library.

Note

If you have existing or inflight development projects, you can continue to use version 10. But for new projects, or to use new features, you should transition to the new library.

About this quickstart

Create a .NET Core console application in C# that creates, loads, and queries an Azure Cognitive Search index using Visual Studio and the Microsoft.Azure.Search client libraries.

This article explains how to create the application. You could also download and run the complete application.

Note

The demo code in this article uses the synchronous methods of the Azure Cognitive Search version 10 .NET SDK for simplicity. However, for production scenarios, we recommend using the asynchronous methods in your own applications to keep them scalable and responsive. For example, you could use CreateAsync and DeleteAsync instead of Create and Delete.

Prerequisites

Before you begin, you must have the following:

Get a key and URL

Calls to the service require a URL endpoint and an access key on every request. A search service is created with both, so if you added Azure Cognitive Search to your subscription, follow these steps to get the necessary information:

  1. Sign in to the Azure portal, and in your search service Overview page, get the URL. An example endpoint might look like https://mydemo.search.windows.net.

  2. In Settings > Keys, get an admin key for full rights on the service. There are two interchangeable admin keys, provided for business continuity in case you need to roll one over. You can use either the primary or secondary key on requests for adding, modifying, and deleting objects.

    Get the query key as well. It's a best practice to issue query requests with read-only access.

Get an HTTP endpoint and access key

All requests require an api-key on every request sent to your service. Having a valid key establishes trust, on a per request basis, between the application sending the request and the service that handles it.

Set up your environment

Begin by opening Visual Studio and creating a new Console App project that can run on .NET Core.

Install NuGet packages

The Microsoft.Azure.Search package consists of a few client libraries that are distributed as NuGet packages.

For this project, use version 10 of the Microsoft.Azure.Search NuGet package and the latest Microsoft.Extensions.Configuration.Json NuGet package.

  1. In Tools > NuGet Package Manager, select Manage NuGet Packages for Solution....

  2. Click Browse.

  3. Search for Microsoft.Azure.Search and select version 10.

  4. Click Install on the right to add the assembly to your project and solution.

  5. Repeat for Microsoft.Extensions.Configuration.Json, selecting version 2.2.0 or later.

Add Azure Cognitive Search service information

  1. In Solution Explorer, right click on the project and select Add > New Item... .

  2. In Add New Item, search for "JSON" to return a JSON-related list of item types.

  3. Choose JSON File, name the file "appsettings.json", and click Add.

  4. Add the file to your output directory. Right-click appsettings.json and select Properties. In Copy to Output Directory, select Copy if newer.

  5. Copy the following JSON into your new JSON file.

    {
      "SearchServiceName": "<YOUR-SEARCH-SERVICE-NAME>",
      "SearchServiceAdminApiKey": "<YOUR-ADMIN-API-KEY>",
      "SearchIndexName": "hotels-quickstart"
    }
    
  6. Replace the search service name (YOUR-SEARCH-SERVICE-NAME) and admin API key (YOUR-ADMIN-API-KEY) with valid values. If your service endpoint is https://mydemo.search.windows.net, the service name would be "mydemo".

Add class ".Method" files to your project

This step is required to produce meaningful output in the console. When printing results to the console window, individual fields from the Hotel object must be returned as strings. This step implements ToString() to perform this task, which you do by copying the necessary code to two new files.

  1. Add two empty class definitions to your project: Address.Methods.cs, Hotel.Methods.cs

  2. In Address.Methods.cs, overwrite the default contents with the following code, lines 1-25.

  3. In Hotel.Methods.cs, copy lines 1-68.

1 - Create index

The hotels index consists of simple and complex fields, where a simple field is "HotelName" or "Description", and complex fields are an address with subfields, or a collection of rooms. When an index includes complex types, isolate the complex field definitions in separate classes.

  1. Add two empty class definitions to your project: Address.cs, Hotel.cs

  2. In Address.cs, overwrite the default contents with the following code:

    using System;
    using Microsoft.Azure.Search;
    using Microsoft.Azure.Search.Models;
    using Newtonsoft.Json;
    
    namespace AzureSearchQuickstart
    {
        public partial class Address
        {
            [IsSearchable]
            public string StreetAddress { get; set; }
    
            [IsSearchable, IsFilterable, IsSortable, IsFacetable]
            public string City { get; set; }
    
            [IsSearchable, IsFilterable, IsSortable, IsFacetable]
            public string StateProvince { get; set; }
    
            [IsSearchable, IsFilterable, IsSortable, IsFacetable]
            public string PostalCode { get; set; }
    
            [IsSearchable, IsFilterable, IsSortable, IsFacetable]
            public string Country { get; set; }
        }
    }
    
  3. In Hotel.cs, the class defines the overall structure of the index, including references to the address class.

    namespace AzureSearchQuickstart
    {
        using System;
        using Microsoft.Azure.Search;
        using Microsoft.Azure.Search.Models;
        using Newtonsoft.Json;
    
        public partial class Hotel
        {
            [System.ComponentModel.DataAnnotations.Key]
            [IsFilterable]
            public string HotelId { get; set; }
    
            [IsSearchable, IsSortable]
            public string HotelName { get; set; }
    
            [IsSearchable]
            [Analyzer(AnalyzerName.AsString.EnMicrosoft)]
            public string Description { get; set; }
    
            [IsSearchable]
            [Analyzer(AnalyzerName.AsString.FrLucene)]
            [JsonProperty("Description_fr")]
            public string DescriptionFr { get; set; }
    
            [IsSearchable, IsFilterable, IsSortable, IsFacetable]
            public string Category { get; set; }
    
            [IsSearchable, IsFilterable, IsFacetable]
            public string[] Tags { get; set; }
    
            [IsFilterable, IsSortable, IsFacetable]
            public bool? ParkingIncluded { get; set; }
    
            [IsFilterable, IsSortable, IsFacetable]
            public DateTimeOffset? LastRenovationDate { get; set; }
    
            [IsFilterable, IsSortable, IsFacetable]
            public double? Rating { get; set; }
    
            public Address Address { get; set; }
        }
    }
    

    Attributes on the field determine how it is used in an application. For example, the IsSearchable attribute must be assigned to every field that should be included in a full text search.

    Note

    In the .NET SDK, fields must be explicitly attributed as IsSearchable, IsFilterable, IsSortable, and IsFacetable. This behavior is in contrast with the REST API which implicitly enables attribution based on data type (for example, simple string fields are automatically searchable).

    Exactly one field in your index of type string must be the key field, uniquely identifying each document. In this schema, the key is HotelId.

    In this index, the description fields use the optional analyzer property, specified when you want to override the default standard Lucene analyzer. The description_fr field is using the French Lucene analyzer (FrLucene) because it stores French text. The description is using the optional Microsoft language analyzer (EnMicrosoft).

  4. In Program.cs, create an instance of the SearchServiceClient class to connect to the service, using values that are stored in the application's config file (appsettings.json).

    SearchServiceClient has an Indexes property, providing all the methods you need to create, list, update, or delete Azure Cognitive Search indexes.

    using System;
    using System.Linq;
    using System.Threading;
    using Microsoft.Azure.Search;
    using Microsoft.Azure.Search.Models;
    using Microsoft.Extensions.Configuration;
    
    namespace AzureSearchQuickstart
    {
        class Program {
            // Demonstrates index delete, create, load, and query
            // Commented-out code is uncommented in later steps
            static void Main(string[] args)
            {
                IConfigurationBuilder builder = new ConfigurationBuilder().AddJsonFile("appsettings.json");
                IConfigurationRoot configuration = builder.Build();
    
                SearchServiceClient serviceClient = CreateSearchServiceClient(configuration);
    
                string indexName = configuration["SearchIndexName"];
    
                Console.WriteLine("{0}", "Deleting index...\n");
                DeleteIndexIfExists(indexName, serviceClient);
    
                Console.WriteLine("{0}", "Creating index...\n");
                CreateIndex(indexName, serviceClient);
    
                // Uncomment next 3 lines in "2 - Load documents"
                // ISearchIndexClient indexClient = serviceClient.Indexes.GetClient(indexName);
                // Console.WriteLine("{0}", "Uploading documents...\n");
                // UploadDocuments(indexClient);
    
                // Uncomment next 2 lines in "3 - Search an index"
                // Console.WriteLine("{0}", "Searching index...\n");
                // RunQueries(indexClient);
    
                Console.WriteLine("{0}", "Complete.  Press any key to end application...\n");
                Console.ReadKey();
            }
    
            // Create the search service client
            private static SearchServiceClient CreateSearchServiceClient(IConfigurationRoot configuration)
            {
                string searchServiceName = configuration["SearchServiceName"];
                string adminApiKey = configuration["SearchServiceAdminApiKey"];
    
                SearchServiceClient serviceClient = new SearchServiceClient(searchServiceName, new SearchCredentials(adminApiKey));
                return serviceClient;
            }
    
            // Delete an existing index to reuse its name
            private static void DeleteIndexIfExists(string indexName, SearchServiceClient serviceClient)
            {
                if (serviceClient.Indexes.Exists(indexName))
                {
                    serviceClient.Indexes.Delete(indexName);
                }
            }
    
            // Create an index whose fields correspond to the properties of the Hotel class.
            // The Address property of Hotel will be modeled as a complex field.
            // The properties of the Address class in turn correspond to sub-fields of the Address complex field.
            // The fields of the index are defined by calling the FieldBuilder.BuildForType() method.
            private static void CreateIndex(string indexName, SearchServiceClient serviceClient)
            {
                var definition = new Microsoft.Azure.Search.Models.Index()
                {
                    Name = indexName,
                    Fields = FieldBuilder.BuildForType<Hotel>()
                };
    
                serviceClient.Indexes.Create(definition);
            }
        }
    }    
    

    If possible, share a single instance of SearchServiceClient in your application to avoid opening too many connections. Class methods are thread-safe to enable such sharing.

    The class has several constructors. The one you want takes your search service name and a SearchCredentials object as parameters. SearchCredentials wraps your api-key.

    In the index definition, the easiest way to create the Field objects is by calling the FieldBuilder.BuildForType method, passing a model class for the type parameter. A model class has properties that map to the fields of your index. This mapping allows you to bind documents from your search index to instances of your model class.

    Note

    If you don't plan to use a model class, you can still define your index by creating Field objects directly. You can provide the name of the field to the constructor, along with the data type (or analyzer for string fields). You can also set other properties like IsSearchable, IsFilterable, to name a few.

  5. Press F5 to build the app and create the index.

    If the project builds successfully, a console window opens, writing status messages to the screen for deleting and creating the index.

2 - Load documents

In Azure Cognitive Search, documents are data structures that are both inputs to indexing and outputs from queries. As obtained from an external data source, document inputs might be rows in a database, blobs in Blob storage, or JSON documents on disk. In this example, we're taking a shortcut and embedding JSON documents for four hotels in the code itself.

When uploading documents, you must use an IndexBatch object. An IndexBatch contains a collection of IndexAction objects, each of which contains a document and a property telling Azure Cognitive Search what action to perform (upload, merge, delete, and mergeOrUpload).

  1. In Program.cs, create an array of documents and index actions, and then pass the array to IndexBatch. The documents below conform to the hotel-quickstart index, as defined by the hotel and address classes.

    // Upload documents as a batch
    private static void UploadDocuments(ISearchIndexClient indexClient)
    {
        var actions = new IndexAction<Hotel>[]
        {
            IndexAction.Upload(
                new Hotel()
                {
                    HotelId = "1",
                    HotelName = "Secret Point Motel",
                    Description = "The hotel is ideally located on the main commercial artery of the city in the heart of New York. A few minutes away is Time's Square and the historic centre of the city, as well as other places of interest that make New York one of America's most attractive and cosmopolitan cities.",
                    DescriptionFr = "L'hôtel est idéalement situé sur la principale artère commerciale de la ville en plein cœur de New York. A quelques minutes se trouve la place du temps et le centre historique de la ville, ainsi que d'autres lieux d'intérêt qui font de New York l'une des villes les plus attractives et cosmopolites de l'Amérique.",
                    Category = "Boutique",
                    Tags = new[] { "pool", "air conditioning", "concierge" },
                    ParkingIncluded = false,
                    LastRenovationDate = new DateTimeOffset(1970, 1, 18, 0, 0, 0, TimeSpan.Zero),
                    Rating = 3.6,
                    Address = new Address()
                    {
                        StreetAddress = "677 5th Ave",
                        City = "New York",
                        StateProvince = "NY",
                        PostalCode = "10022",
                        Country = "USA"
                    }
                }
            ),
            IndexAction.Upload(
                new Hotel()
                {
                    HotelId = "2",
                    HotelName = "Twin Dome Motel",
                    Description = "The hotel is situated in a  nineteenth century plaza, which has been expanded and renovated to the highest architectural standards to create a modern, functional and first-class hotel in which art and unique historical elements coexist with the most modern comforts.",
                    DescriptionFr = "L'hôtel est situé dans une place du XIXe siècle, qui a été agrandie et rénovée aux plus hautes normes architecturales pour créer un hôtel moderne, fonctionnel et de première classe dans lequel l'art et les éléments historiques uniques coexistent avec le confort le plus moderne.",
                    Category = "Boutique",
                    Tags = new[] { "pool", "free wifi", "concierge" },
                    ParkingIncluded = false,
                    LastRenovationDate =  new DateTimeOffset(1979, 2, 18, 0, 0, 0, TimeSpan.Zero),
                    Rating = 3.60,
                    Address = new Address()
                    {
                        StreetAddress = "140 University Town Center Dr",
                        City = "Sarasota",
                        StateProvince = "FL",
                        PostalCode = "34243",
                        Country = "USA"
                    }
                }
            ),
            IndexAction.Upload(
                new Hotel()
                {
                    HotelId = "3",
                    HotelName = "Triple Landscape Hotel",
                    Description = "The Hotel stands out for its gastronomic excellence under the management of William Dough, who advises on and oversees all of the Hotel’s restaurant services.",
                    DescriptionFr = "L'hôtel est situé dans une place du XIXe siècle, qui a été agrandie et rénovée aux plus hautes normes architecturales pour créer un hôtel moderne, fonctionnel et de première classe dans lequel l'art et les éléments historiques uniques coexistent avec le confort le plus moderne.",
                    Category = "Resort and Spa",
                    Tags = new[] { "air conditioning", "bar", "continental breakfast" },
                    ParkingIncluded = true,
                    LastRenovationDate = new DateTimeOffset(2015, 9, 20, 0, 0, 0, TimeSpan.Zero),
                    Rating = 4.80,
                    Address = new Address()
                    {
                        StreetAddress = "3393 Peachtree Rd",
                        City = "Atlanta",
                        StateProvince = "GA",
                        PostalCode = "30326",
                        Country = "USA"
                    }
                }
            ),
            IndexAction.Upload(
                new Hotel()
                {
                    HotelId = "4",
                    HotelName = "Sublime Cliff Hotel",
                    Description = "Sublime Cliff Hotel is located in the heart of the historic center of Sublime in an extremely vibrant and lively area within short walking distance to the sites and landmarks of the city and is surrounded by the extraordinary beauty of churches, buildings, shops and monuments. Sublime Cliff is part of a lovingly restored 1800 palace.",
                    DescriptionFr = "Le sublime Cliff Hotel est situé au coeur du centre historique de sublime dans un quartier extrêmement animé et vivant, à courte distance de marche des sites et monuments de la ville et est entouré par l'extraordinaire beauté des églises, des bâtiments, des commerces et Monuments. Sublime Cliff fait partie d'un Palace 1800 restauré avec amour.",
                    Category = "Boutique",
                    Tags = new[] { "concierge", "view", "24-hour front desk service" },
                    ParkingIncluded = true,
                    LastRenovationDate = new DateTimeOffset(1960, 2, 06, 0, 0, 0, TimeSpan.Zero),
                    Rating = 4.6,
                    Address = new Address()
                    {
                        StreetAddress = "7400 San Pedro Ave",
                        City = "San Antonio",
                        StateProvince = "TX",
                        PostalCode = "78216",
                        Country = "USA"
                    }
                }
            ),
        };
    
        var batch = IndexBatch.New(actions);
    
        try
        {
            indexClient.Documents.Index(batch);
        }
        catch (IndexBatchException e)
        {
            // When a service is under load, indexing might fail for some documents in the batch. 
            // Depending on your application, you can compensate by delaying and retrying. 
            // For this simple demo, we just log the failed document keys and continue.
            Console.WriteLine(
                "Failed to index some of the documents: {0}",
                String.Join(", ", e.IndexingResults.Where(r => !r.Succeeded).Select(r => r.Key)));
        }
    
        // Wait 2 seconds before starting queries 
        Console.WriteLine("Waiting for indexing...\n");
        Thread.Sleep(2000);
    }
    

    Once you initialize theIndexBatch object, you can send it to the index by calling Documents.Index on your SearchIndexClient object. Documents is a property of SearchIndexClient that provides methods for adding, modifying, deleting, or querying documents in your index.

    The try/catch surrounding the call to the Index method catches indexing failures, which might happen if your service is under heavy load. In production code, you could delay and then retry indexing the documents that failed, or log and continue like the sample does, or handle it in some other way that meets your application's data consistency requirements.

    The 2-second delay compensates for indexing, which is asynchronous, so that all documents can be indexed before the queries are executed. Coding in a delay is typically only necessary in demos, tests, and sample applications.

  2. In Program.cs, in main, uncomment the lines for "2 - Load documents".

    // Uncomment next 3 lines in "2 - Load documents"
    ISearchIndexClient indexClient = serviceClient.Indexes.GetClient(indexName);
    Console.WriteLine("{0}", "Uploading documents...\n");
    UploadDocuments(indexClient);
    
  3. Press F5 to rebuild the app.

    If the project builds successfully, a console window opens, writing status messages, this time with a message about uploading documents. In the Azure portal, in the search service Overview page, the hotels-quickstart index should now have 4 documents.

For more information about document processing, see "How the .NET SDK handles documents".

3 - Search an index

You can get query results as soon as the first document is indexed, but actual testing of your index should wait until all documents are indexed.

This section adds two pieces of functionality: query logic, and results. For queries, use the Search method. This method takes search text as well as other parameters.

The DocumentsSearchResult class represents the results.

  1. In Program.cs, create a WriteDocuments method that prints search results to the console.

    private static void WriteDocuments(DocumentSearchResult<Hotel> searchResults)
    {
        foreach (SearchResult<Hotel> result in searchResults.Results)
        {
            Console.WriteLine(result.Document);
        }
    
        Console.WriteLine();
    }
    
  2. Create a RunQueries method to execute queries and return results. Results are Hotel objects. You can use the select parameter to surface individual fields. If a field is not included in the select parameter, its corresponding Hotel property will be null.

    private static void RunQueries(ISearchIndexClient indexClient)
    {
        SearchParameters parameters;
        DocumentSearchResult<Hotel> results;
    
        // Query 1 
        Console.WriteLine("Query 1: Search for term 'Atlanta' with no result trimming");
        parameters = new SearchParameters();
        results = indexClient.Documents.Search<Hotel>("Atlanta", parameters);
        WriteDocuments(results);
    
        // Query 2
        Console.WriteLine("Query 2: Search on the term 'Atlanta', with trimming");
        Console.WriteLine("Returning only these fields: HotelName, Tags, Address:\n");
        parameters =
            new SearchParameters()
            {
                Select = new[] { "HotelName", "Tags", "Address" },
            };
        results = indexClient.Documents.Search<Hotel>("Atlanta", parameters);
        WriteDocuments(results);
    
        // Query 3
        Console.WriteLine("Query 3: Search for the terms 'restaurant' and 'wifi'");
        Console.WriteLine("Return only these fields: HotelName, Description, and Tags:\n");
        parameters =
            new SearchParameters()
            {
                Select = new[] { "HotelName", "Description", "Tags" }
            };
        results = indexClient.Documents.Search<Hotel>("restaurant, wifi", parameters);
        WriteDocuments(results);
    
        // Query 4 -filtered query
        Console.WriteLine("Query 4: Filter on ratings greater than 4");
        Console.WriteLine("Returning only these fields: HotelName, Rating:\n");
        parameters =
            new SearchParameters()
            {
                Filter = "Rating gt 4",
                Select = new[] { "HotelName", "Rating" }
            };
        results = indexClient.Documents.Search<Hotel>("*", parameters);
        WriteDocuments(results);
    
        // Query 5 - top 2 results
        Console.WriteLine("Query 5: Search on term 'boutique'");
        Console.WriteLine("Sort by rating in descending order, taking the top two results");
        Console.WriteLine("Returning only these fields: HotelId, HotelName, Category, Rating:\n");
        parameters =
            new SearchParameters()
            {
                OrderBy = new[] { "Rating desc" },
                Select = new[] { "HotelId", "HotelName", "Category", "Rating" },
                Top = 2
            };
        results = indexClient.Documents.Search<Hotel>("boutique", parameters);
        WriteDocuments(results);
    }
    

    There are two ways of matching terms in a query: full-text search, and filters. A full-text search query searches for one or more terms in IsSearchable fields in your index. A filter is a boolean expression that is evaluated over IsFilterable fields in an index. You can use full-text search and filters together or separately.

    Both searches and filters are performed using the Documents.Search method. A search query can be passed in the searchText parameter, while a filter expression can be passed in the Filter property of the SearchParameters class. To filter without searching, just pass "*" for the searchText parameter. To search without filtering, just leave the Filter property unset, or do not pass in a SearchParameters instance at all.

  3. In Program.cs, in main, uncomment the lines for "3 - Search".

    // Uncomment next 2 lines in "3 - Search an index"
    Console.WriteLine("{0}", "Searching documents...\n");
    RunQueries(indexClient);
    
  4. The solution is now finished. Press F5 to rebuild the app and run the program in its entirety.

    Output includes the same messages as before, with addition of query information and results.

Clean up resources

When you're working in your own subscription, it's a good idea at the end of a project to identify whether you still need the resources you created. Resources left running can cost you money. You can delete resources individually or delete the resource group to delete the entire set of resources.

You can find and manage resources in the portal, using the All resources or Resource groups link in the left-navigation pane.

If you are using a free service, remember that you are limited to three indexes, indexers, and data sources. You can delete individual items in the portal to stay under the limit.

Next steps

In this C# quickstart, you worked through a series of tasks to create an index, load it with documents, and run queries. At different stages, we took shortcuts to simplify the code for readability and comprehension. If you are comfortable with the basic concepts, we recommend the next article for an exploration of alternative approaches and concepts that will deepen your knowledge.

The sample code and index are expanded versions of this one. The next sample adds a Rooms collection, uses different classes and actions, and takes a closer look at how processing works.

Want to optimize and save on your cloud spending?